Skip to content

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)),
)
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.

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.

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.