Queries
Nothing runs when you build a query. That is the whole design: predicates can be added on a branch, which is what static query generators structurally cannot express.
q := sqlb.Query[Post]().Where(sqlb.F("status").Eq("published"))if search != "" { q = q.Where(sqlb.F("title").Contains(search))}posts, err := q.OrderBy(sqlb.F("created_at").Desc()).Limit(50).All(ctx, db)Methods mutate the builder and return it, so a query can be assembled across
branches without reassignment gymnastics — and so a hook can amend a query it is
handed. Use Clone() before sharing a partially built query between goroutines
or request scopes.
Where skips the zero Pred, so If removes the surrounding statement
entirely when a filter is optional:
q.Where( sqlb.F("status").Eq("published"), sqlb.If(minViews > 0, sqlb.F("view_count").Gte(minViews)),)Terminal methods
Section titled “Terminal methods”| Method | Returns |
|---|---|
All(ctx, db) |
Every matching row |
One(ctx, db) |
The single match; ErrNotFound if none, an error if more than one |
First(ctx, db) |
The first match; pair it with OrderBy to be deterministic |
Count(ctx, db) |
Row count, ignoring pagination; group count for a grouped query |
Exists(ctx, db) |
Whether anything matched |
SQL() |
The statement and its bind parameters, executing nothing |
One fetches two rows so it can tell you the result was ambiguous rather than
silently returning the first — a caller asking for one row is asserting only one
exists.
The builder is cloned before hooks run, so running the same builder twice does not accumulate their predicates.
Predicates
Section titled “Predicates”sqlb.F("column") is the untyped reference; the generated PostCols.Title is
the typed one, and worth preferring — see Typed columns.
Comparison: Eq, Neq, Gt, Gte, Lt, Lte, Between, NotBetween,
OneOf, NotOneOf, IsNull, NotNull, EqField.
Text: Contains, StartsWith, EndsWith, Like, ILike.
Contains, StartsWith and EndsWith escape LIKE metacharacters, so a user
typing 50% searches for that literal string. Like and ILike do not — use
them only for patterns your own code wrote.
Eq(nil) becomes IS NULL rather than = NULL, which is never true and is
never what the caller meant.
Combine with And, Or and Not. All three skip zero predicates, and Not of
a zero predicate stays zero, so an absent filter stays absent rather than
becoming always-false.
Aggregates and other shapes
Section titled “Aggregates and other shapes”Collect[R] scans into a type other than the model, which is how grouped
queries are read:
type Revenue struct { Status string `db:"status"` Total float64 `db:"revenue"`}
rows, err := sqlb.Collect[Revenue](ctx, db, sqlb.Query[Order](). GroupBy(sqlb.F("status")). Select(sqlb.F("status"), sqlb.Sum(sqlb.F("total")).As("revenue")))Query hooks still run, so tenant scoping applies to aggregates too. Unlike
All, Collect requires every field of R to be filled by some result column:
R was written to match this projection, so an unfilled field is a mistyped
alias rather than a deliberate partial select — and a mistyped alias on a Sum
would otherwise report zero revenue silently.
Raw and RawPred are the escape hatch for expressions the builder cannot
model. Their contents are not validated; their ? placeholders are renumbered
by the compiler.
- Typed columns — putting column names under the compiler
- Paging — walking a whole result set without
OFFSET - Mutations and transactions — inserts, updates,
WithTx - Hooks — where domain logic lives
- Inspecting and tracing —
SQL(),Explain, wrapping the executor - Testing an application on sqlb — the database-free double, and what it can and cannot answer