Documentation
¶
Overview ¶
Package storm is the public surface: the types you declare a model with, and the builder that turns those models into a schema.
Index ¶
- Constants
- func ArgsOf(d RawDecl) int
- func Build(models ...any) (*schema.Schema, error)
- func DeclOf(d RawDecl) (reflect.Type, string)
- func RegisterScanner[T any](fn func([][]byte, *T, *runtime.Slab) error)
- func RegisterStatement(sql string)
- type Action
- type AggregateBuilder
- func (b *AggregateBuilder) Avg(x any, as string) Out
- func (b *AggregateBuilder) AvgOver(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) By(fieldPtrs ...any) *AggregateBuilder
- func (b *AggregateBuilder) ByExpr(as string, t Term) Out
- func (b *AggregateBuilder) Compute(as string, t Term) Out
- func (b *AggregateBuilder) Count(as string) Out
- func (b *AggregateBuilder) CountDistinct(fieldPtr any, as string) Out
- func (b *AggregateBuilder) CountOf(fieldPtr any, as string) Out
- func (b *AggregateBuilder) Cube() *AggregateBuilder
- func (b *AggregateBuilder) CumeDist(as string, w *WindowSpec) Out
- func (b *AggregateBuilder) DenseRank(as string, w *WindowSpec) Out
- func (b *AggregateBuilder) FirstValue(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) GroupingOf(as string, fieldPtrs ...any) Out
- func (b *AggregateBuilder) Having(c Cond) *AggregateBuilder
- func (b *AggregateBuilder) Lag(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) LastValue(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) Lead(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) Max(x any, as string) Out
- func (b *AggregateBuilder) MaxOver(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) Min(x any, as string) Out
- func (b *AggregateBuilder) MinOver(x any, as string, w *WindowSpec) Out
- func (b *AggregateBuilder) OrderAsc(o Out) *AggregateBuilder
- func (b *AggregateBuilder) OrderDesc(o Out) *AggregateBuilder
- func (b *AggregateBuilder) Param(name string) Term
- func (b *AggregateBuilder) PercentRank(as string, w *WindowSpec) Out
- func (b *AggregateBuilder) Rank(as string, w *WindowSpec) Out
- func (b *AggregateBuilder) Rollup() *AggregateBuilder
- func (b *AggregateBuilder) RowNumber(as string, w *WindowSpec) Out
- func (b *AggregateBuilder) Sets(sets ...[]string) *AggregateBuilder
- func (b *AggregateBuilder) Sum(x any, as string) Out
- func (b *AggregateBuilder) SumOver(x any, as string, w *WindowSpec) Out
- type Aggregates
- type Aggregator
- type AnyRef
- type ColBuilder
- func (b *ColBuilder) AcknowledgeNoFK(reason string) *ColBuilder
- func (b *ColBuilder) Cidr() *ColBuilder
- func (b *ColBuilder) Comment(s string) *ColBuilder
- func (b *ColBuilder) Date() *ColBuilder
- func (b *ColBuilder) Default(e Expr) *ColBuilder
- func (b *ColBuilder) Generated(e Expr) *ColBuilder
- func (b *ColBuilder) Immutable() *ColBuilder
- func (b *ColBuilder) Index() *ColBuilder
- func (b *ColBuilder) Named(n string) *ColBuilder
- func (b *ColBuilder) NotNull() *ColBuilder
- func (b *ColBuilder) Nullable() *ColBuilder
- func (b *ColBuilder) Numeric(p, s int) *ColBuilder
- func (b *ColBuilder) OnDelete(a Action) *ColBuilder
- func (b *ColBuilder) OnUpdate(a Action) *ColBuilder
- func (b *ColBuilder) Raw(sqlType string) *ColBuilder
- func (b *ColBuilder) Size(n int) *ColBuilder
- func (b *ColBuilder) Unique() *ColBuilder
- func (b *ColBuilder) Version() *ColBuilder
- type Cond
- type Decimal
- type Enumer
- type ExcludeBuilder
- type ExcludeSpec
- type Exprdeprecated
- type Exprs
- func (Exprs) Abs(x any) Term
- func (Exprs) Add(l, r any) Term
- func (Exprs) And(cs ...Cond) Cond
- func (Exprs) Coalesce(args ...any) Term
- func (Exprs) Col(fieldPtr any) Term
- func (Exprs) CurrentRow() FrameBound
- func (Exprs) DateTrunc(unit string, ts any) Term
- func (Exprs) Div(l, r any) Term
- func (Exprs) DivScale(l, r any, scale int) Term
- func (Exprs) Eq(l, r any) Cond
- func (Exprs) Following(n int) FrameBound
- func (Exprs) Grouping(fieldPtrs ...any) Term
- func (Exprs) Gt(l, r any) Cond
- func (Exprs) Gte(l, r any) Cond
- func (Exprs) IsNotNull(x any) Cond
- func (Exprs) IsNull(x any) Cond
- func (Exprs) Lit(v any) Term
- func (Exprs) Lower(x any) Term
- func (Exprs) Lt(l, r any) Cond
- func (Exprs) Lte(l, r any) Cond
- func (Exprs) Mul(l, r any) Term
- func (Exprs) Ne(l, r any) Cond
- func (Exprs) Not(c Cond) Cond
- func (Exprs) NullIf(a, b any) Term
- func (Exprs) OnCols(alias, column string, fieldPtr any) JoinOn
- func (Exprs) Or(cs ...Cond) Cond
- func (Exprs) Over() *WindowSpec
- func (Exprs) Preceding(n int) FrameBound
- func (Exprs) Sub(l, r any) Term
- func (Exprs) UnboundedFollowing() FrameBound
- func (Exprs) UnboundedPreceding() FrameBound
- func (Exprs) Upper(x any) Term
- type FrameBound
- type IndexBuilder
- type IndexColumn
- type Interval
- type JoinBuilder
- func (b *JoinBuilder) Inner(model any, on any) *JoinBuilder
- func (b *JoinBuilder) InnerWith(alias string, on JoinOn) *JoinBuilder
- func (b *JoinBuilder) Left(model any, on any) *JoinBuilder
- func (b *JoinBuilder) LeftWith(alias string, on JoinOn) *JoinBuilder
- func (b *JoinBuilder) OrderAsc(fieldPtr any) *JoinBuilder
- func (b *JoinBuilder) OrderDesc(fieldPtr any) *JoinBuilder
- func (b *JoinBuilder) Take(fieldPtr any, as string) *JoinBuilder
- func (b *JoinBuilder) TakeFrom(alias, column, as string) *JoinBuilder
- func (b *JoinBuilder) Where(c Cond) *JoinBuilder
- func (b *JoinBuilder) With(alias string, model any, aggregate string) *JoinBuilder
- type JoinOn
- type Joiner
- type Joins
- type Model
- type Nested
- type Null
- type OneOf2
- type OneOf3
- type OneOf4
- type OneOf5
- type OneOf6
- type OneOf7
- type OneOf8
- type Out
- type PlanBuilder
- type Planner
- type Plans
- type Projections
- type Projector
- type RawDecl
- type RawSQL
- type SQLQuery
- type SQLStmt
- type Schemer
- type TSVector
- type Table
- func (t *Table) Check(e Expr) *Table
- func (t *Table) Col(fieldPtr any) *ColBuilder
- func (t *Table) Comment(s string) *Table
- func (t *Table) Exclude(parts ...ExcludeSpec) *ExcludeBuilder
- func (t *Table) Index(cols ...any) *IndexBuilder
- func (t *Table) Name(n string) *Table
- func (t *Table) PrimaryKey(fields ...any) *Table
- func (t *Table) Through(fieldPtr any, join any) *Table
- func (t *Table) Unique(cols ...any) *Table
- type Term
- type TimeOfDay
- type TstzRange
- type UUID
- type UnionBranchSpec
- type UnionDecl
- type UnionSpec
- type WindowSpec
Constants ¶
const ( OpEq = "=" OpOverlaps = "&&" OpAdjacent = "-|-" )
Exclusion operators.
const ( BTree = "btree" GIN = "gin" GiST = "gist" Hash = "hash" BRIN = "brin" )
Index methods.
const MaxTimeOfDay = runtime.MaxTimeOfDay
MaxTimeOfDay is 24:00:00, which PostgreSQL accepts as a `time`.
Variables ¶
This section is empty.
Functions ¶
func ArgsOf ¶ added in v0.5.0
ArgsOf reports how many arguments a declaration will demand at the call.
The count comes from scanning the statement for the highest $n, which is a guess: `$1` inside a string literal or a dollar-quoted body is text to PostgreSQL and a placeholder to a scanner. The generator PREPAREs every declaration and the server reports the real number, so the two are compared at generate time and a disagreement fails the build — which is the whole reason this is reachable from outside the package.
func Build ¶
Build turns a set of model structs into a schema. Pass pointers to zero values: Build(&User{}, &Org{}, &Post{}).
Every declaration problem is collected and reported together, because failing on the first one would mean N build cycles to find N mistakes.
func DeclOf ¶
DeclOf reads a registered query's row type and SQL; the generate command uses it and nothing else should.
func RegisterScanner ¶
RegisterScanner is called by generated code from an init(). One scanner per row type: two queries sharing a row type share its scanner, which is safe because the generator validated both against the same descriptor shape.
func RegisterStatement ¶ added in v0.5.0
func RegisterStatement(sql string)
RegisterStatement records a statement that `storm generate` PREPAREd and validated against the model. Generated code calls it from an init().
It takes the statement TEXT rather than a digest so the generated init is reviewable: what a reader needs to check is which statements are allowed to run, and a list of hex is not that.
Types ¶
type AggregateBuilder ¶ added in v0.3.0
type AggregateBuilder struct {
// contains filtered or unexported fields
}
AggregateBuilder accumulates one declaration.
func (*AggregateBuilder) AvgOver ¶ added in v0.5.0
func (b *AggregateBuilder) AvgOver(x any, as string, w *WindowSpec) Out
func (*AggregateBuilder) By ¶ added in v0.3.0
func (b *AggregateBuilder) By(fieldPtrs ...any) *AggregateBuilder
By groups by columns. The field name is derived from the column name.
func (*AggregateBuilder) ByExpr ¶ added in v0.3.0
func (b *AggregateBuilder) ByExpr(as string, t Term) Out
ByExpr groups by an expression, which needs a name because date_trunc('day', placed_at) has no obvious one.
func (*AggregateBuilder) Compute ¶ added in v0.5.0
func (b *AggregateBuilder) Compute(as string, t Term) Out
Compute adds an output that is an EXPRESSION over the group — the ratio, the difference, the share — rather than a single aggregate:
orders := b.Count("Orders")
paid := b.Count("Paid").Filter(a.Eq(&o.Status, "paid"))
b.Compute("PaidRate", a.Div(paid, a.NullIf(orders, a.Lit(0))))
The NullIf is not decoration. It is the division-by-zero guard, and it sits in the expression instead of in a comment above it.
The result type is whatever the expression resolves to, so a ratio over two counts is numeric — not the truncated integer PostgreSQL's `/` would give.
func (*AggregateBuilder) Count ¶ added in v0.3.0
func (b *AggregateBuilder) Count(as string) Out
Count adds count(*): rows per group, never NULL.
func (*AggregateBuilder) CountDistinct ¶ added in v0.5.0
func (b *AggregateBuilder) CountDistinct(fieldPtr any, as string) Out
CountDistinct adds count(DISTINCT col): how many DIFFERENT values the column takes, which is a third question again from Count (rows) and CountOf (rows where it is not null).
DISTINCT is offered for count and nothing else. `sum(DISTINCT x)` and `avg(DISTINCT x)` are legal SQL and almost always a bug — the sum of the distinct values of a price column is not a number anyone wanted — so having them would cost more than it bought.
It cannot be combined with a window: PostgreSQL rejects DISTINCT in an aggregate used as a window function, and storm refuses it at declaration rather than emitting SQL the server will not plan.
func (*AggregateBuilder) CountOf ¶ added in v0.3.0
func (b *AggregateBuilder) CountOf(fieldPtr any, as string) Out
CountOf adds count(col), which counts rows where the column is NOT NULL — a different question from Count, and a common bug, so a different method.
func (*AggregateBuilder) Cube ¶ added in v0.3.0
func (b *AggregateBuilder) Cube() *AggregateBuilder
Cube is ROLLUP's every-combination sibling: 2ⁿ grouping sets.
func (*AggregateBuilder) CumeDist ¶ added in v0.5.0
func (b *AggregateBuilder) CumeDist(as string, w *WindowSpec) Out
func (*AggregateBuilder) DenseRank ¶ added in v0.3.0
func (b *AggregateBuilder) DenseRank(as string, w *WindowSpec) Out
func (*AggregateBuilder) FirstValue ¶ added in v0.3.0
func (b *AggregateBuilder) FirstValue(x any, as string, w *WindowSpec) Out
FirstValue is the first row's value in the window.
func (*AggregateBuilder) GroupingOf ¶ added in v0.3.0
func (b *AggregateBuilder) GroupingOf(as string, fieldPtrs ...any) Out
GroupingOf adds GROUPING(cols...) — 1 when the row is a subtotal over those columns, 0 when the value is real.
func (*AggregateBuilder) Having ¶ added in v0.3.0
func (b *AggregateBuilder) Having(c Cond) *AggregateBuilder
func (*AggregateBuilder) Lag ¶ added in v0.3.0
func (b *AggregateBuilder) Lag(x any, as string, w *WindowSpec) Out
Lag and Lead read the previous or next row in the window. Both are NULL at the partition edge however non-null the column is, so both produce a nullable field.
func (*AggregateBuilder) LastValue ¶ added in v0.5.0
func (b *AggregateBuilder) LastValue(x any, as string, w *WindowSpec) Out
LastValue reads the last row of the window frame — and the frame, not the partition, is the point. PostgreSQL's default frame ends at the CURRENT ROW, so `last_value` without a frame returns the current row's own value, which is the single most reported surprise in window functions. Give it a frame that reaches the end:
a.Over().OrderByAsc(day).Rows(a.UnboundedPreceding(), a.UnboundedFollowing())
func (*AggregateBuilder) Lead ¶ added in v0.3.0
func (b *AggregateBuilder) Lead(x any, as string, w *WindowSpec) Out
func (*AggregateBuilder) MaxOver ¶ added in v0.5.0
func (b *AggregateBuilder) MaxOver(x any, as string, w *WindowSpec) Out
func (*AggregateBuilder) MinOver ¶ added in v0.5.0
func (b *AggregateBuilder) MinOver(x any, as string, w *WindowSpec) Out
func (*AggregateBuilder) OrderAsc ¶ added in v0.5.0
func (b *AggregateBuilder) OrderAsc(o Out) *AggregateBuilder
Having filters the GROUPS, after aggregation. A call-site Where filters the rows that go INTO the groups; these are different questions and mixing them up silently changes the answer. OrderAsc and OrderDesc order the result by a declared output, replacing the default ordering — the grouping columns, in declaration order.
This is what makes a top-N report one statement. A grouped read can only be ordered by something in its select list, so before an output could be the sort key the only orderings available were the ones the grouping already gave; "the ten products by revenue" meant fetching every product and sorting in Go, which is a LIMIT the database never sees.
b := a.Named("TopProducts")
b.By(&p.SKU)
rev := b.Sum(&p.Amount, "Revenue")
b.OrderDesc(rev)
rows, err := product.New().Limit(10).AllTopProducts(ctx, ex)
The handle has to come from THIS aggregation: ordering by an output of another one is a build error naming both, not SQL that fails on first use.
func (*AggregateBuilder) OrderDesc ¶ added in v0.5.0
func (b *AggregateBuilder) OrderDesc(o Out) *AggregateBuilder
OrderDesc orders descending — see OrderAsc.
func (*AggregateBuilder) Param ¶ added in v0.5.0
func (b *AggregateBuilder) Param(name string) Term
Param declares a value the CALL supplies, for use inside a Filter or a Having:
since := b.Param("Since")
b.Count("Recent").Filter(a.Gte(&e.OccurredAt, since))
rows, err := event.New().AllRates(ctx, ex, time.Now().Add(-30*24*time.Hour))
A FILTER is part of the declaration, so its condition is fixed at generate time — which makes "the last thirty days" unsayable, because that is relative to when the query runs. This is the narrow answer: the aggregation still has ONE shape, one compiled statement and one scanner; only a value varies.
The type is inferred from the column the parameter is first compared with, so the generated signature cannot disagree with what it filters. Declared parameters are numbered before the call-site predicates, and appear in the generated function in declaration order.
func (*AggregateBuilder) PercentRank ¶ added in v0.5.0
func (b *AggregateBuilder) PercentRank(as string, w *WindowSpec) Out
PercentRank and CumeDist are the fractional ranks: where a row sits in its window as a number between 0 and 1, which is what a percentile report wants and what Rank cannot give without knowing the partition size.
Both are float8 and never NULL — an empty window produces no rows to rank.
func (*AggregateBuilder) Rank ¶ added in v0.3.0
func (b *AggregateBuilder) Rank(as string, w *WindowSpec) Out
func (*AggregateBuilder) Rollup ¶ added in v0.3.0
func (b *AggregateBuilder) Rollup() *AggregateBuilder
Rollup turns the grouping into ROLLUP(...): every prefix of the grouping columns plus a grand total, in one pass instead of one query per level.
Every grouping column becomes NULLABLE in the row type, because a subtotal row carries NULL for the columns it aggregated over. Use storm.Grouping to tell that NULL from one that was in the data.
func (*AggregateBuilder) RowNumber ¶ added in v0.3.0
func (b *AggregateBuilder) RowNumber(as string, w *WindowSpec) Out
RowNumber, Rank and DenseRank number rows within the window.
func (*AggregateBuilder) Sets ¶ added in v0.3.0
func (b *AggregateBuilder) Sets(sets ...[]string) *AggregateBuilder
Sets declares explicit grouping sets by the names given to By/ByExpr. An empty set is the grand total row.
This is the answer to N+1 queries per facet: one pass over the table produces every facet count, instead of one query per facet.
func (*AggregateBuilder) Sum ¶ added in v0.3.0
func (b *AggregateBuilder) Sum(x any, as string) Out
Sum, Avg, Min and Max are the ordinary aggregates. All four are NULL over zero rows, so all four produce a nullable field.
func (*AggregateBuilder) SumOver ¶ added in v0.5.0
func (b *AggregateBuilder) SumOver(x any, as string, w *WindowSpec) Out
SumOver, AvgOver, MinOver and MaxOver aggregate ACROSS THE GROUPS — the running total, the moving average, the high-water mark:
rev := b.Sum(&o.Total, "Revenue")
b.AvgOver(rev, "Moving7", a.Over().OrderByAsc(day).
Rows(a.Preceding(6), a.CurrentRow()))
The argument is a declared output, and that is the whole distinction. A grouped query has already collapsed its rows, so `sum(total) OVER (...)` reads a column that no longer exists per output row and PostgreSQL refuses it; `sum(sum(total)) OVER (...)` is the form that means "across the groups", and passing the handle is what produces it.
Give them a frame. Without one PostgreSQL's default reaches from the start of the partition to the current row, which makes every one of these a RUNNING figure rather than a moving one.
type Aggregates ¶ added in v0.3.0
type Aggregates struct {
// Exprs is the declaration-time expression vocabulary: a.Eq, a.DateTrunc,
// a.Over and the rest. Embedded rather than exported at package level so a
// declaration constructor cannot be reached from a query context.
Exprs
// contains filtered or unexported fields
}
Named aggregations: a GROUP BY and the expressions over it, declared once.
func (o *Order) Aggregates(a *storm.Aggregates) {
a.Named("Daily").
ByExpr("Day", storm.DateTrunc("day", &o.PlacedAt)).
Count("Orders").
Count("Paid").Filter(storm.Eq(&o.Status, StatusPaid)).
Sum(&o.Total, "Revenue").
RowNumber("Rank", storm.Over().OrderByDesc(&o.Total))
}
rows, err := order.New().
Where(order.PlacedAt.Gte(since)). // call-site predicates still compose
AllDaily(ctx, ex) // []order.DailyRow
**Declared, not composed at the call site**, for the reason the library exists: a `GroupBy(...).Select(...)` chain assembled at run time has an unbounded set of result shapes, and a shape storm has not seen can have neither a generated scanner nor a compiled statement. Naming it keeps the whole thing inside the compilation thesis. The call-site predicates stay dynamic because those ARE bounded.
func (*Aggregates) Named ¶ added in v0.3.0
func (a *Aggregates) Named(name string) *AggregateBuilder
Named starts an aggregation. The generated type is this name plus "Row", so "Daily" becomes DailyRow in the table's package.
type Aggregator ¶ added in v0.3.0
type Aggregator interface {
Aggregates(*Aggregates)
}
Aggregator is implemented by models that declare aggregations. Optional.
type AnyRef ¶ added in v0.5.0
type AnyRef struct{}
AnyRef is the DISCRIMINATOR form of polymorphism: a (type, id) pair naming a row in any table at all.
Two columns — `<field>_type` and `<field>_id` — and no foreign key, because no database can constrain one. Nothing stops the id naming a row that does not exist, or the type naming a table that does not either. That is not a gap in storm; it is what the shape costs, and it is why OneOf is the default.
storm will not generate it silently. A model declaring an AnyRef without calling AcknowledgeNoFK fails Build, naming the field and the two ways out. "We gave up referential integrity" belongs in a diff, and a required call is the only place a reviewer is guaranteed to see it.
The variants are unbounded, which is the one thing OneOf cannot offer past about eight. When integrity matters and the variant count does not fit a column each, the answer is a supertype table — full integrity, no arity limit, one extra insert.
Zero-sized, like every OneOfN, and for the same reason: the model is a DECLARATION. The two columns it stands for appear in the generated Row as SubjectType and SubjectID, exactly as an arc's variants appear as their own key columns — the row carries columns, not the declaration that produced them.
type ColBuilder ¶
type ColBuilder struct {
// contains filtered or unexported fields
}
ColBuilder configures one column.
func (*ColBuilder) AcknowledgeNoFK ¶ added in v0.5.0
func (b *ColBuilder) AcknowledgeNoFK(reason string) *ColBuilder
AcknowledgeNoFK records why this AnyRef gives up referential integrity.
Required: Build refuses an AnyRef without one. The reason travels into the schema and out through `storm diff`, so the decision is visible where it is reviewed rather than only where it was made.
func (*ColBuilder) Cidr ¶
func (b *ColBuilder) Cidr() *ColBuilder
Cidr narrows a netip.Prefix column from inet to cidr — the database then rejects host bits, which is the entire difference between the two types.
func (*ColBuilder) Comment ¶
func (b *ColBuilder) Comment(s string) *ColBuilder
func (*ColBuilder) Date ¶
func (b *ColBuilder) Date() *ColBuilder
Date narrows a time.Time column to a calendar date. The Go type stays time.Time (there is no stdlib date), decoded as midnight UTC.
func (*ColBuilder) Default ¶
func (b *ColBuilder) Default(e Expr) *ColBuilder
func (*ColBuilder) Generated ¶
func (b *ColBuilder) Generated(e Expr) *ColBuilder
func (*ColBuilder) Immutable ¶
func (b *ColBuilder) Immutable() *ColBuilder
func (*ColBuilder) Index ¶
func (b *ColBuilder) Index() *ColBuilder
Index adds a single-column index.
func (*ColBuilder) Named ¶
func (b *ColBuilder) Named(n string) *ColBuilder
func (*ColBuilder) NotNull ¶
func (b *ColBuilder) NotNull() *ColBuilder
func (*ColBuilder) Nullable ¶
func (b *ColBuilder) Nullable() *ColBuilder
func (*ColBuilder) Numeric ¶
func (b *ColBuilder) Numeric(p, s int) *ColBuilder
func (*ColBuilder) OnDelete ¶
func (b *ColBuilder) OnDelete(a Action) *ColBuilder
OnDelete and OnUpdate set the referential action of this column's foreign key.
func (*ColBuilder) OnUpdate ¶
func (b *ColBuilder) OnUpdate(a Action) *ColBuilder
func (*ColBuilder) Raw ¶
func (b *ColBuilder) Raw(sqlType string) *ColBuilder
Raw forces a database type storm does not model.
func (*ColBuilder) Size ¶
func (b *ColBuilder) Size(n int) *ColBuilder
func (*ColBuilder) Unique ¶
func (b *ColBuilder) Unique() *ColBuilder
Unique adds a single-column unique constraint. On a foreign key this is what turns one-to-many into one-to-one.
func (*ColBuilder) Version ¶
func (b *ColBuilder) Version() *ColBuilder
type Cond ¶ added in v0.3.0
type Cond struct {
// contains filtered or unexported fields
}
Cond is a DECLARED predicate, used by Filter, Having and a join's Where.
Distinct from a call-site Where, which is dynamic: that varies per call and is a token stream spliced into a cached statement with bound arguments. A declared predicate never varies, so it is rendered into the text.
type Decimal ¶
Decimal is an exact fixed-point number for a numeric column.
An alias, not a wrapper: the model declares storm.Decimal and generated code reads runtime.Decimal, and those must be the same type or every value would need converting at the boundary storm exists to remove.
float64 is not offered for numeric. It cannot represent 0.10, and an accounting system that rounds is a defect rather than a tolerance — so the choice is made once, here, instead of by whoever writes the model.
func ParseDecimal ¶
ParseDecimal reads a decimal from its text form.
type Enumer ¶
type Enumer interface {
EnumValues() []string
}
Enumer marks a named type as a Postgres enum. Constants are not discoverable through reflection, so the type has to list them.
type Status string
const (StatusActive Status = "active"; StatusBanned Status = "banned")
func (Status) EnumValues() []string { return []string{"active", "banned"} }
type ExcludeBuilder ¶
type ExcludeBuilder struct {
// contains filtered or unexported fields
}
ExcludeBuilder configures an exclusion constraint.
func (*ExcludeBuilder) Named ¶
func (b *ExcludeBuilder) Named(n string) *ExcludeBuilder
func (*ExcludeBuilder) Using ¶
func (b *ExcludeBuilder) Using(method string) *ExcludeBuilder
func (*ExcludeBuilder) Where ¶
func (b *ExcludeBuilder) Where(e Expr) *ExcludeBuilder
type ExcludeSpec ¶
type ExcludeSpec struct {
// contains filtered or unexported fields
}
ExcludeSpec is one `<column-or-expression> WITH <operator>` part.
func With ¶
func With(field any, op string) ExcludeSpec
With pairs a field with an exclusion operator.
func WithExpr ¶
func WithExpr(e RawSQL, op string) ExcludeSpec
WithExpr pairs an expression with an exclusion operator, for the range overlap that scalar columns cannot express:
t.Exclude(storm.With(&b.Room, storm.OpEq),
storm.WithExpr("tstzrange(starts_at, ends_at)", storm.OpOverlaps))
type Exprs ¶ added in v0.4.0
type Exprs struct{}
Exprs is the declaration-time expression vocabulary, reached through the `*Aggregates` or `*Joins` value a declaration is handed.
Methods rather than package functions, and deliberately. These were `storm.Eq`, `storm.And`, `storm.Col` and nineteen more at the top level, where they sat beside the generated query API meaning something different: `order.Status.Eq(x)` filters rows at run time, `storm.Eq(&o.Status, x)` described a filter at declaration time. Two `Eq`s in scope with different semantics is a question every reader has to answer once. Hanging them off the builder answers it structurally — a declaration constructor cannot be reached from a query, because the builder is not in scope there.
func (Exprs) Add ¶ added in v0.5.0
Add, Sub, Mul and Div are arithmetic over two terms — the ratio a report asks for, written where the division is.
Div on two integers resolves to NUMERIC, not to an integer. PostgreSQL's `/` truncates, so `Div(paid, total)` over two counts would otherwise be 0 for every group that is not entirely paid — a plausible-looking wrong answer, which is the only kind that matters.
Division by zero is still an error at the server, and NullIf is the guard:
a.Div(recent, a.NullIf(prior, a.Lit(0))) // NULL, not a failed query
func (Exprs) And ¶ added in v0.4.0
And, Or and Not compose conditions. Always parenthesised when rendered: AND/OR precedence is a classic source of silently wrong predicates and the brackets cost nothing.
func (Exprs) Col ¶ added in v0.4.0
Col is an explicit column reference. Rarely needed: a field pointer is accepted directly wherever a Term is.
func (Exprs) CurrentRow ¶ added in v0.5.0
func (Exprs) CurrentRow() FrameBound
func (Exprs) DateTrunc ¶ added in v0.4.0
DateTrunc buckets a timestamp — the reason grouping takes an expression at all. `unit` is a PostgreSQL field name: "hour", "day", "month", "year".
func (Exprs) DivScale ¶ added in v0.5.0
DivScale is Div with the scale said out loud — money that needs more than six places, or a percentage that wants two.
func (Exprs) Eq ¶ added in v0.4.0
Eq and friends compare a column, a declared output or an expression against a literal.
func (Exprs) Following ¶ added in v0.5.0
func (Exprs) Following(n int) FrameBound
func (Exprs) Grouping ¶ added in v0.4.0
Grouping reports, per row, whether a grouping set aggregated over these columns. It is how a ROLLUP's subtotal NULL is told apart from a NULL that was in the data — without it a subtotal row and a real NULL group are indistinguishable, which is a wrong answer that looks like a right one.
func (Exprs) Lit ¶ added in v0.4.0
Lit is a declaration-time constant.
Rendered into the statement rather than bound, because it comes from the declaration and never varies — which is what keeps a filtered aggregate one cached statement instead of one per value.
func (Exprs) Lower ¶ added in v0.5.0
Lower and Upper case-fold text. Useful in a grouping expression, where "Ada" and "ada" are one group or two and the answer has to be said.
func (Exprs) NullIf ¶ added in v0.4.0
NullIf returns NULL when the two arguments are equal — the division-by-zero guard, written where the division is rather than in a comment above it.
func (Exprs) OnCols ¶ added in v0.4.0
OnCols joins on a named column of an aliased scope — a CTE's output column, or a table's column — against a field of the model being attached.
func (Exprs) Preceding ¶ added in v0.5.0
func (Exprs) Preceding(n int) FrameBound
func (Exprs) UnboundedFollowing ¶ added in v0.5.0
func (Exprs) UnboundedFollowing() FrameBound
func (Exprs) UnboundedPreceding ¶ added in v0.5.0
func (Exprs) UnboundedPreceding() FrameBound
UnboundedPreceding, CurrentRow and UnboundedFollowing are the fixed frame edges; Preceding and Following take a count of rows.
type FrameBound ¶ added in v0.5.0
type FrameBound = schema.FrameBound
FrameBound is one edge of a window frame, built by the methods below.
type IndexBuilder ¶
type IndexBuilder struct {
// contains filtered or unexported fields
}
IndexBuilder configures an index after Index(...).
func (*IndexBuilder) Named ¶
func (b *IndexBuilder) Named(n string) *IndexBuilder
func (*IndexBuilder) Unique ¶
func (b *IndexBuilder) Unique() *IndexBuilder
func (*IndexBuilder) Using ¶
func (b *IndexBuilder) Using(method string) *IndexBuilder
func (*IndexBuilder) Where ¶
func (b *IndexBuilder) Where(e Expr) *IndexBuilder
type IndexColumn ¶
type IndexColumn struct {
// contains filtered or unexported fields
}
IndexColumn is a field reference with ordering or an expression applied.
func Lower ¶
func Lower(field any) IndexColumn
Lower indexes lower(col) — the usual answer for case-insensitive uniqueness.
func NullsLast ¶
func NullsLast(ic IndexColumn) IndexColumn
NullsLast puts NULLs at the end of an index key.
type Interval ¶
Interval is a PostgreSQL interval: months, days and microseconds kept separate, because a month has no fixed length and a day is not always 24 hours. An alias for the same reason Decimal is — the model's type and the generated code's type must be one type.
type JoinBuilder ¶ added in v0.3.0
type JoinBuilder struct {
// contains filtered or unexported fields
}
JoinBuilder accumulates one declaration.
func (*JoinBuilder) Inner ¶ added in v0.3.0
func (b *JoinBuilder) Inner(model any, on any) *JoinBuilder
Inner and Left attach a table.
`on` is either a relation field pointer on the declaring model — the FK says how to join, so there is nothing to spell — or an explicit condition built with storm.OnCols.
func (*JoinBuilder) InnerWith ¶ added in v0.3.0
func (b *JoinBuilder) InnerWith(alias string, on JoinOn) *JoinBuilder
InnerWith and LeftWith attach a CTE that With() put in scope.
With() materialises it; these say how it joins. Two steps because a CTE can be referenced by more than one join condition, and because "declare it" and "attach it" are genuinely different decisions.
func (*JoinBuilder) Left ¶ added in v0.3.0
func (b *JoinBuilder) Left(model any, on any) *JoinBuilder
Left keeps every row of the left side. Every column taken from the right becomes nullable in the generated row, which is what a LEFT JOIN means.
func (*JoinBuilder) LeftWith ¶ added in v0.3.0
func (b *JoinBuilder) LeftWith(alias string, on JoinOn) *JoinBuilder
LeftWith keeps rows with no matching CTE row. Everything taken from the CTE becomes nullable, which is what a LEFT join means — and for an aggregate CTE it is usually the right choice, because a customer with no orders has no row in a GROUP BY over orders.
func (*JoinBuilder) OrderAsc ¶ added in v0.3.0
func (b *JoinBuilder) OrderAsc(fieldPtr any) *JoinBuilder
OrderAsc and OrderDesc order the joined result.
A join has no natural order, and an unordered multi-table result shuffles between requests — the same reason an aggregation orders by its grouping.
func (*JoinBuilder) OrderDesc ¶ added in v0.3.0
func (b *JoinBuilder) OrderDesc(fieldPtr any) *JoinBuilder
func (*JoinBuilder) Take ¶ added in v0.3.0
func (b *JoinBuilder) Take(fieldPtr any, as string) *JoinBuilder
Take adds a column to the output. The field pointer may be into the declaring model or into any joined one.
func (*JoinBuilder) TakeFrom ¶ added in v0.3.0
func (b *JoinBuilder) TakeFrom(alias, column, as string) *JoinBuilder
TakeFrom adds a column from an aliased scope — a CTE's aggregate output.
func (*JoinBuilder) Where ¶ added in v0.3.0
func (b *JoinBuilder) Where(c Cond) *JoinBuilder
Where declares a predicate the caller cannot widen. Call-site predicates still compose and are ANDed with it.
func (*JoinBuilder) With ¶ added in v0.3.0
func (b *JoinBuilder) With(alias string, model any, aggregate string) *JoinBuilder
With materialises a declared aggregation as a CTE.
var o Order
j.Named("VsSpend").
With("spend", &o, "ByCustomer").
Inner(&c, storm.OnCols("spend", "customer_id", &c.ID))
One pass over the aggregated table, reused by the join, instead of a correlated subquery per row.
type JoinOn ¶ added in v0.3.0
type JoinOn struct {
// contains filtered or unexported fields
}
JoinOn is an explicit join condition.
type Joiner ¶ added in v0.3.0
type Joiner interface {
Joins(*Joins)
}
Joiner is implemented by models that declare joins. Optional.
type Joins ¶ added in v0.3.0
type Joins struct {
// Exprs is the declaration-time expression vocabulary: j.Ne, j.OnCols and
// the rest. Embedded for the same reason Aggregates embeds it — so a
// declaration constructor cannot be reached from a query context.
Exprs
// contains filtered or unexported fields
}
Named joins: one read that projects across tables.
func (o *Order) Joins(j *storm.Joins) {
var c Customer
j.Named("WithCustomer").
Inner(&c, &o.Customer). // the FK relation says how to join
Take(&o.ID, "OrderID").
Take(&o.Total, "Total").
Take(&c.Email, "Email").
OrderDesc(&o.PlacedAt)
}
rows, err := order.New().
Where(order.PlacedAt.Gte(since)). // call-site predicates still compose
AllWithCustomer(ctx, ex) // []order.WithCustomerRow
**A join projects; it does not load entities.** The output is a flat row of scalars, because a join answers a question and materialising two entity types to answer it is the round-tripping this exists to avoid. When you want the entities, that is a Plan — one query per relation, no fan-out.
The joined model is a LOCAL variable. Taking field pointers into it is what makes `&c.Email` a checked reference rather than a string, so a rename of Customer.Email is a compile error here too.
func (*Joins) Named ¶ added in v0.3.0
func (j *Joins) Named(name string) *JoinBuilder
Named starts a join. The generated type is this name plus "Row".
type Model ¶
Model is the conventional embedded primary key and timestamps. Embedding it is optional — declare your own key if you want a natural one.
It carries its own Schema method, so embedding it is the whole declaration: the key gets a default and is immutable, and both timestamps default to now().
type Nested ¶
type Nested struct {
// contains filtered or unexported fields
}
Nested names a relation on a table OTHER than the one declaring the plan — a post's comments, reached through a user's posts.
It exists because a field pointer needs an instance to point into, and the declaring model has no Post to hand. Into supplies one: the builder already allocated a zero value of every registered model, so the closure is called with that and the offset resolves against the right table.
type Null ¶
Null is the allocation-free nullable. A `*T` in a model becomes a Null[T] in the generated row type: a pointer would cost one allocation per non-nil field per row, and rows are the hot path.
type OneOf2 ¶
type OneOf2[A, B any] struct { // contains filtered or unexported fields }
OneOf2 is an exclusive arc over two variants.
type OneOf3 ¶
type OneOf3[A, B, C any] struct { // contains filtered or unexported fields }
OneOf3 is an exclusive arc over three variants.
type OneOf4 ¶
type OneOf4[A, B, C, D any] struct { // contains filtered or unexported fields }
OneOf4 is an exclusive arc over four variants.
type OneOf5 ¶
type OneOf5[A, B, C, D, E any] struct { // contains filtered or unexported fields }
OneOf5 is an exclusive arc over five variants.
type OneOf6 ¶
type OneOf6[A, B, C, D, E, F any] struct { // contains filtered or unexported fields }
OneOf6 is an exclusive arc over six variants.
type OneOf7 ¶
type OneOf7[A, B, C, D, E, F, G any] struct { // contains filtered or unexported fields }
OneOf7 is an exclusive arc over seven variants.
type OneOf8 ¶
type OneOf8[A, B, C, D, E, F, G, H any] struct { // contains filtered or unexported fields }
OneOf8 is an exclusive arc over eight variants.
type Out ¶ added in v0.3.0
type Out struct {
// contains filtered or unexported fields
}
Out is a reference to a declared output, returned by the method that declared it.
It replaces a string lookup. `Having(a.Gt(a.Out("Orders"), 0))` named an output that storm checked at build time; a handle is checked by the Go compiler, and it cannot name an output that has not been declared yet because you do not have one until it has.
func (Out) Filter ¶ added in v0.4.0
Filter restricts THIS aggregate to the rows matching cond — `count(*) FILTER (WHERE status = 'paid')`, which is both clearer and faster than `count(CASE WHEN ...)`.
Attached to the output it filters rather than to "the last one declared", so moving a line cannot silently move the filter with it.
func (Out) OverWindow ¶ added in v0.4.0
func (o Out) OverWindow(w *WindowSpec) Out
OverWindow attaches a window to THIS aggregate — a moving total, or the classic "share of the group" without a self-join.
type PlanBuilder ¶
type PlanBuilder struct {
// contains filtered or unexported fields
}
PlanBuilder collects the relations one plan loads.
func (*PlanBuilder) With ¶
func (b *PlanBuilder) With(relPtr any, nested ...Nested) *PlanBuilder
With adds a relation to the plan, addressed by field pointer. Anything passed after it is loaded THROUGH it and costs one more round trip each.
type Planner ¶
type Planner interface {
Plans(*Plans)
}
Planner is implemented by models that declare fetch plans. Optional: a model with no Plans method gets the one-plan-per-relation tier, which is finite by construction and needs no declaration.
type Plans ¶
type Plans struct {
// contains filtered or unexported fields
}
Named fetch plans.
A plan says which relations are loaded together, and the generator emits a distinct type per plan whose fields are exactly those relations. Reading an unloaded relation is then a compile error rather than an empty slice.
You name the plans. That is the whole answer to the projection-type explosion: generating a type per `With(...)` combination is 2ⁿ per entity, and the fix is not a cleverer generator but a shorter list — the one the developer actually uses.
It also makes `plans.go` the single reviewable file listing every load pattern in a system, which is a thing no other Go ORM has, and which a linter can cost in round trips.
func (u *User) Plans(p *storm.Plans) {
p.Named("Feed").With(&u.Posts).With(&u.Org)
}
Relations are named by FIELD POINTER, like everything else in the declaration API, so the editor enforces them and a rename follows.
func (*Plans) Named ¶
func (p *Plans) Named(name string) *PlanBuilder
Named starts a plan. The generated type is the model name plus this name, so "Feed" on User becomes UserFeed.
type Projections ¶
type Projections struct {
// contains filtered or unexported fields
}
Named projections: read less than the whole row, by name.
func (u *User) Projections(p *storm.Projections) {
p.Named("Contact", &u.Email, &u.Name)
}
rows, err := user.New().Where(...).AllContact(ctx, ex) // []user.ContactRow
The full-row read is the safe default and the expensive one: every column travels, TOAST'd values are fetched whether or not anyone looks, and an index-only scan is impossible by construction. A named projection is the declared, reviewable subset — same predicates, same ordering, same keyset machinery, narrower tuple, its own generated row type and scanner.
Named rather than a Select(cols...) builder for R3's reason: a type per combination is 2ⁿ per entity, and the fix is a shorter list — the one the code actually uses.
func (*Projections) Named ¶
func (p *Projections) Named(name string, fieldPtrs ...any)
Named declares one projection over the given column fields.
type Projector ¶
type Projector interface {
Projections(*Projections)
}
Projector is implemented by models that declare projections. Optional.
type RawDecl ¶
type RawDecl interface {
// contains filtered or unexported methods
}
RawDecl is implemented by every SQLQuery, so a bootstrap can register them as a plain []any the way it registers models.
type RawSQL ¶ added in v0.4.0
type RawSQL string
RawSQL is a raw SQL fragment. It is the one deliberate escape from the typed API: conspicuous, reported by `storm lint --expr`, and validated against the database at generate time.
Named for what it is. It was `Expr`, which is what someone reaching for "an expression" types — and they would land on the untyped escape hatch rather than on the checked expression vocabulary, which lives on the declaration builder (a.DateTrunc, a.Coalesce, a.Eq).
func GenRandomUUID ¶
func GenRandomUUID() RawSQL
GenRandomUUID renders gen_random_uuid(), built in since PostgreSQL 13. This is the default for an embedded storm.Model because it works everywhere the rest of storm does.
type SQLQuery ¶
type SQLQuery[T any] struct { // contains filtered or unexported fields }
The typed escape hatch (M5).
Anything PostgreSQL can run is expressible here — CTEs, windows, lateral joins — years before the native IR grows each construct. What storm adds is the part hand-rolled SQL always loses: the RESULT is typed, the scanner is generated, and the statement was validated against the model at GENERATE time, so a query whose columns drifted from its row type fails the build naming the column, not the 3am page.
var TopEarners = storm.SQL[EarnerRow](`
WITH ranked AS (...)
SELECT ... WHERE tenant_id = $1 ... LIMIT $2`)
rows, err := TopEarners.Query(ctx, db, tid, 3) // []EarnerRow
How the scanner arrives ¶
`storm generate` PREPAREs the statement, matches the result descriptor against T's fields, and emits a scanner that registers itself by type in an init(). The first Query looks it up once and caches it in the value; the warm path is an atomic load. Running a query nothing generated for is an error naming the fix, not a reflective fallback — one reflection path becomes THE path.
Only a declared statement runs ¶
The same generate step emits a RegisterStatement for every statement it PREPAREd, and a declaration whose text is not among them is refused before the executor is reached. That is what keeps the escape hatch from being one: a scanner is keyed by ROW TYPE and would otherwise answer for a statement assembled at run time. See the statement-pinning note further down this file.
type SQLStmt ¶
type SQLStmt struct {
// contains filtered or unexported fields
}
SQLStmt is the no-rows half of the escape hatch: DELETEs, junction-table INSERTs, `SELECT maintenance_fn(...)` calls. It carries no row type — and the generator enforces that, failing generation if the statement's result descriptor has columns, so "I meant to read those rows" cannot compile into silently dropping them.
type Schemer ¶
type Schemer interface {
Schema(t *Table)
}
Schemer is implemented by models that need more than their Go types can say.
The receiver MUST be a pointer. With a value receiver Go copies the struct before the method runs, so &u.Email points into the copy and cannot be resolved back to a field — the builder rejects that at Build time rather than producing a silently wrong schema.
type TSVector ¶ added in v0.3.0
type TSVector struct{}
TSVector is a full-text search column.
Declare one and PostgreSQL gets a `tsvector`; the generated query API gets Matches and WebSearch on it. It is deliberately an EMPTY struct: a tsvector is index support, not data, so it never appears in a Row, never travels on a read, and cannot be written from Go. The usual declaration makes the database maintain it:
Search storm.TSVector
t.Col(&p.Search).
Generated(storm.RawSQL(`to_tsvector('english', coalesce(name,''))`)).
Index()
Nothing else in storm has this shape, and that is the point: a column you can only ask questions of.
type Table ¶
type Table struct {
// contains filtered or unexported fields
}
Table is the builder handed to a model's Schema method. Every reference to a field is a *field pointer* (&u.Email), so a rename is a compile error and a typo never compiles.
func (*Table) Col ¶
func (t *Table) Col(fieldPtr any) *ColBuilder
Col addresses a column by field pointer and returns a builder for it.
t.Col(&u.Email).Unique().Size(320)
func (*Table) Exclude ¶
func (t *Table) Exclude(parts ...ExcludeSpec) *ExcludeBuilder
Exclude adds an exclusion constraint — the correct answer to booking and scheduling overlap, and reachable from no other Go ORM.
t.Exclude(storm.With(&b.Room, storm.OpEq), storm.With(&b.Period, storm.OpOverlaps))
func (*Table) Index ¶
func (t *Table) Index(cols ...any) *IndexBuilder
Index adds a secondary index. Wrap a field in Desc(...) or Lower(...) to order or transform it.
func (*Table) PrimaryKey ¶
PrimaryKey overrides the inferred key. Pass several field pointers for a composite key.
func (*Table) Through ¶ added in v0.5.0
Through declares a many-to-many that runs over a join model the adopter wrote, rather than a table storm generated.
The reason to write the join yourself is that it carries columns of its own — when a role was granted, when it expires — and those columns are the point. A generated join table has nowhere to put them.
func (u *User) Schema(t *storm.Table) {
t.Through(&u.Roles, UserRole{})
}
`join` is a VALUE of the join model, not a pointer and not a field pointer: it names a type, and the type is all storm needs. The join model must carry exactly one foreign key to each end, which is what makes the two directions unambiguous — a join with two keys to the same table is a self-referential shape that has to name its own columns.
The plan row carries the join row and the far row together, so the payload is reachable: `u.Roles[i].GrantedAt` alongside `u.Roles[i].Role.Name`.
type Term ¶ added in v0.3.0
type Term struct {
// contains filtered or unexported fields
}
Term is an expression inside a declaration: a column, a literal, a scalar function, or a reference to another declared output.
Resolved and TYPED at generation time, which is the point — the generated row's field type is whatever the Term turns out to be.
Field pointers are accepted anywhere a Term is, so the common case reads as itself: a.DateTrunc("day", &o.PlacedAt).
type TimeOfDay ¶
TimeOfDay is a PostgreSQL `time` — microseconds since midnight, no date and no zone. See runtime.TimeOfDay for why it is not a time.Time.
func NewTimeOfDay ¶
NewTimeOfDay builds a time of day from its parts, reporting false for parts out of range rather than normalising them — 25:00 is a mistake, not 01:00 tomorrow.
type TstzRange ¶ added in v0.3.0
TstzRange is a PostgreSQL tstzrange: an interval of time with explicit bounds, so "do these two bookings overlap" is a question the database answers with an index rather than four comparisons in Go that get the boundary cases wrong.
An alias, not a wrapper, for the same reason Decimal is: the model declares storm.TstzRange and generated code reads runtime.TstzRange, and those must be the same type or every value would need converting at the boundary storm exists to remove.
func NewTstzRange ¶ added in v0.3.0
NewTstzRange builds the half-open range [lower, upper) — the one scheduling wants, because adjacent slots then do not collide on the instant they touch.
type UUID ¶
type UUID [16]byte
UUID keeps the core dependency-free. Map it to your preferred package once with a codec rather than importing one here.
type UnionBranchSpec ¶ added in v0.5.0
type UnionBranchSpec struct {
// contains filtered or unexported fields
}
UnionBranchSpec is one branch of a union under construction.
func (*UnionBranchSpec) Const ¶ added in v0.5.0
func (b *UnionBranchSpec) Const(as string, v any) *UnionBranchSpec
Const projects a literal, which is how a merged feed carries the tag saying which branch a row came from. Without it the rows are indistinguishable once they are merged, and the caller is left inferring the source from which fields happen to be set.
func (*UnionBranchSpec) Take ¶ added in v0.5.0
func (b *UnionBranchSpec) Take(fieldPtr any, as string) *UnionBranchSpec
Take projects a column into the output column named as.
Every branch must project the same names in the same order. That is what union-compatible means, and storm checks it rather than trusting the declaration: two branches whose third column is `Text` in one and `Kind` in the other produce a row type where half the values are in the wrong field, and PostgreSQL will not object as long as the types line up.
func (*UnionBranchSpec) Where ¶ added in v0.5.0
func (b *UnionBranchSpec) Where(c Cond) *UnionBranchSpec
Where filters this branch. Declared, so it is fixed in the statement text — there is no call-site predicate on a union, because a predicate over several branches would have to say which one it filtered.
type UnionDecl ¶ added in v0.5.0
type UnionDecl struct {
// contains filtered or unexported fields
}
UnionDecl is a declared union, before it has been resolved against a schema. Pass it to Build alongside the models.
func Union ¶ added in v0.5.0
A declared UNION: several tables projected into one row shape.
var Feed = storm.Union("Feed", func(u *storm.UnionSpec) {
var c Comment
b := u.From(&c)
b.Take(&c.CreatedAt, "OccurredAt")
b.Take(&c.Body, "Text")
b.Const("Kind", "comment")
var r Release
b2 := u.From(&r)
b2.Take(&r.PublishedAt, "OccurredAt")
b2.Take(&r.Notes, "Text")
b2.Const("Kind", "release")
u.OrderDesc("OccurredAt")
})
Why this is a package-level var and not a method ¶
Every other cross-table read hangs off a DRIVING table: a declared join is a method on the model that declares it, and its row type lives in that model's package. A union has no such centre. In a feed of comments, follows and releases none of the three is the one the others attach to, and declaring it on whichever sorted first would put the row type in a package with no more claim to it than the other two. So a union is registered against the schema (ADR-0008).
Why a closure ¶
The branches need LOCAL model instances to take field pointers into, exactly as a Joins method does — and a package-level var cannot have locals. The closure runs during Build, when the tables exist and the pointers can be resolved; nothing is evaluated at package init but the name.
type UnionSpec ¶ added in v0.5.0
type UnionSpec struct {
// contains filtered or unexported fields
}
UnionSpec is a union under construction.
func (*UnionSpec) Distinct ¶ added in v0.5.0
Distinct selects UNION over UNION ALL.
ALL is the default, inverting SQL's. De-duplicating means sorting or hashing the whole result before a single row comes back, and a feed never wants it; a caller who does says so here.
func (*UnionSpec) From ¶ added in v0.5.0
func (s *UnionSpec) From(model any) *UnionBranchSpec
From starts a branch reading model's table.
func (*UnionSpec) OrderAsc ¶ added in v0.5.0
OrderAsc and OrderDesc order the MERGED rows. They name output columns: after the branches are unioned the source tables' own names are gone, and an output alias is the only thing left in scope.
func (*UnionSpec) Param ¶ added in v0.5.0
Param declares a value the CALL supplies, and returns a handle to use in a branch filter:
actor := u.Param("Actor")
orders.Where(storm.Exprs{}.Eq(&o.CustomerID, actor))
The parameter has no type of its own — it takes the type of the column it is first compared with, so the generated function's signature cannot disagree with the column it filters. A parameter that is never used is refused: it would sit in the signature demanding an argument that reaches no statement.
The same parameter used in two branches is ONE argument and one placeholder. That is the point: "this actor's feed" means the same actor in every branch, and making the caller pass it once per branch invites passing two different values.
type WindowSpec ¶ added in v0.3.0
type WindowSpec struct {
// contains filtered or unexported fields
}
WindowSpec is an OVER clause under construction.
func (*WindowSpec) OrderByAsc ¶ added in v0.3.0
func (w *WindowSpec) OrderByAsc(xs ...any) *WindowSpec
OrderByAsc and OrderByDesc order rows WITHIN the partition.
Named this way rather than taking Asc/Desc because those already mean index ordering, and one word meaning two things in one declaration is how a wrong index gets built.
func (*WindowSpec) OrderByDesc ¶ added in v0.3.0
func (w *WindowSpec) OrderByDesc(xs ...any) *WindowSpec
func (*WindowSpec) PartitionBy ¶ added in v0.3.0
func (w *WindowSpec) PartitionBy(xs ...any) *WindowSpec
PartitionBy restarts the window for each distinct value.
func (*WindowSpec) Range ¶ added in v0.5.0
func (w *WindowSpec) Range(start, end FrameBound) *WindowSpec
Range frames by PEERS — rows the ORDER BY cannot tell apart count as one.
Offsets are refused here. `RANGE 7 PRECEDING` needs exactly one ORDER BY column of a type that can be subtracted, and the failure is a server error at the first call rather than anything storm could name; ROWS expresses the same intent with a rule that always holds. Use Range for the unbounded edges, which is what it is actually good for.
func (*WindowSpec) Rows ¶ added in v0.5.0
func (w *WindowSpec) Rows(start, end FrameBound) *WindowSpec
Rows frames the window by COUNTED ROWS: `Rows(a.Preceding(6), a.CurrentRow())` is a seven-row moving window, which is the moving average everyone wants and the default frame cannot express.
Without a frame PostgreSQL uses RANGE from the partition start to the current row — so a running total is the default and a moving one is not, and last_value() reads the current row rather than the last.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
bench
|
|
|
entbench/schema
Package schema is the Ent declaration for the shared bench table.
|
Package schema is the Ent declaration for the shared bench table. |
|
cmd
|
|
|
genbench
command
|
|
|
genspike
command
Command genspike generates the two table packages the M3 plan-type spike sits on top of.
|
Command genspike generates the two table packages the M3 plan-type spike sits on top of. |
|
storm
command
Command storm is the tool: install it once, run it in your module, write no bootstrap.
|
Command storm is the tool: install it once, run it in your module, write no bootstrap. |
|
Package codegen emits Go from the schema IR.
|
Package codegen emits Go from the schema IR. |
|
compile
|
|
|
myddl
Package myddl renders a schema as MySQL 8 DDL.
|
Package myddl renders a schema as MySQL 8 DDL. |
|
pgddl
Package pgddl lowers the schema IR to PostgreSQL DDL.
|
Package pgddl lowers the schema IR to PostgreSQL DDL. |
|
pgsql
Package pgsql lowers query structure to PostgreSQL text.
|
Package pgsql lowers query structure to PostgreSQL text. |
|
examples
|
|
|
blog/gen
command
Command gen regenerates the example's store from its model.
|
Command gen regenerates the example's store from its model. |
|
blog/model
Package model is the quickstart's schema: two tables, one relation, and the declared plans and projections this example reads with.
|
Package model is the quickstart's schema: two tables, one relation, and the declared plans and projections this example reads with. |
|
internal
|
|
|
aliasrow
Package aliasrowx deliberately declares a name that differs from its directory (aliasrow).
|
Package aliasrowx deliberately declares a name that differs from its directory (aliasrow). |
|
benchmodel
Package benchmodel is the M0 benchmark table, declared as a model so the generator can be measured against the hand-written spike on identical SQL.
|
Package benchmodel is the M0 benchmark table, declared as a model so the generator can be measured against the hand-written spike on identical SQL. |
|
testmodel
Package testmodel is the fixture domain used by M1's round-trip tests.
|
Package testmodel is the fixture domain used by M1's round-trip tests. |
|
Package migrate diffs two schemas and emits a reviewable migration.
|
Package migrate diffs two schemas and emits a reviewable migration. |
|
Package runtime is what generated code calls at query time.
|
Package runtime is what generated code calls at query time. |
|
mydec
Package mydec decodes MySQL's binary result protocol.
|
Package mydec decodes MySQL's binary result protocol. |
|
pgxdrv
Package pgxdrv is the only package in storm that knows pgx exists.
|
Package pgxdrv is the only package in storm that knows pgx exists. |
|
Package schema is storm's schema IR: the single representation every front end produces and every back end consumes.
|
Package schema is storm's schema IR: the single representation every front end produces and every back end consumes. |
|
pg
Package pg reads a live PostgreSQL database into storm's schema IR.
|
Package pg reads a live PostgreSQL database into storm's schema IR. |
|
Package tool is the developer-facing command set — render DDL, diff a migration, generate packages, and verify that model, generated code, migrations and the live database all still agree — as a LIBRARY.
|
Package tool is the developer-facing command set — render DDL, diff a migration, generate packages, and verify that model, generated code, migrations and the live database all still agree — as a LIBRARY. |
|
bootstrap
Package toolbootstrap writes and runs the bootstrap main that adopters used to keep in their own repositories.
|
Package toolbootstrap writes and runs the bootstrap main that adopters used to keep in their own repositories. |
|
discover
Package tooldiscover finds an adopter's models in their source, so the tool does not have to be handed them.
|
Package tooldiscover finds an adopter's models in their source, so the tool does not have to be handed them. |