Reading data
This page covers the read half of the query builder: typed predicates, boolean composition, ordering, pagination, and the terminals that run a SELECT and hand you rows. It starts from query.Select[T](sess) and the Product model introduced on the hub page.
For exhaustive API detail, see the reference at pkg.go.dev/liteorm.org/query.
Typed predicates
Section titled “Typed predicates”The heart of the builder is Filter, which takes one or more typed, column-validated predicates. Build a predicate from a typed column token — query.Col[V]("name") — and an operator. The value type V is checked at compile time, and the column name is validated against your model’s schema when the query runs (an unknown column is a clear error, never silent SQL).
hot, err := query.Select[Product](db). Filter( query.Col[string]("category").Eq("electronics"), query.Col[float64]("price").Gt(50), ). OrderBy("price DESC"). All(ctx)Multiple predicates passed to Filter — or stacked across several Filter calls — are joined with AND.
The operators
Section titled “The operators”| I want… | Predicate |
|---|---|
| equality / inequality | .Eq(v) .Ne(v) |
| ordering comparisons | .Gt(v) .Ge(v) .Lt(v) .Le(v) |
| pattern match (raw pattern) | .Like("%pro%") |
| literal substring (wildcards escaped) | .HasPrefix("foo") .HasSuffix(".go") .Contains("ab") |
| set membership | .In(a, b, c) .NotIn(a, b) |
| NULL tests | .IsNull() .IsNotNull() |
| column vs column | .EqCol(query.Col[V]("x").Of("t")) |
| IN a subquery | .InQuery(sub) .NotInQuery(sub) |
HasPrefix/HasSuffix/Contains escape any %/_ in the needle so it matches literally — the safe way to do a prefix/suffix/contains search on user input, where .Like would treat those characters as wildcards. They render col LIKE ? ESCAPE '~' and are portable across all four backends. The .InQuery / .EqCol forms belong to joins & subqueries.
On SQLite, query.Match(col, q) adds the MATCH operator for FTS5 / spellfix1 / sqlite-vec virtual tables (see SQLite search); it composes in Filter like any predicate but is rejected at build time on other dialects. Typed JSON/JSONB and array predicates come from query.JSON(...) and query.Array[E](...) — see Postgres features.
Waiving column validation
Section titled “Waiving column validation”A predicate’s column is checked against your model’s fields — a typo is a build-time error. For a column that exists on the underlying table but isn’t a model field (a virtual table’s HIDDEN constraint columns, the implicit rowid/oid), chain .Unvalidated() to skip only that check; the column stays typed and dialect-quoted:
query.Col[int]("scope").Unvalidated().Le(2) // renders "scope" <= ?Combining predicates: And / Or / Not
Section titled “Combining predicates: And / Or / Not”Filter’s top-level AND covers the common case. For anything richer, compose explicitly with query.And, query.Or, and query.Not, which nest to any depth:
mixed, err := query.Select[Product](db). Filter(query.Or( query.Col[string]("category").In("books", "home"), query.Col[string]("name").Like("%Pro%"), )). OrderBy("name"). All(ctx)query.Not(p) negates a single predicate. Each group is parenthesized, so query.Or(...) stays atomic when it’s AND-joined with your other conditions.
Ordering and pagination
Section titled “Ordering and pagination”Order takes typed terms — query.Asc(col) / query.Desc(col) — each validated against the model and quoted by the dialect. OrderBy is the raw-string escape hatch (so you control collation, functions, NULLS FIRST); the two compose in call order. Limit and Offset page the result.
page, err := query.Select[Product](db). Filter(query.Col[bool]("active").Eq(true)). Order(query.Desc(query.Col[float64]("price")), query.Asc(query.Col[string]("name"))). Limit(20). Offset(40). All(ctx)
// raw escape hatch (e.g. a collation or a NULLS ordering the typed form can't express):recent, _ := query.Select[Product](db).OrderBy("created_at DESC NULLS LAST").All(ctx)Terminals: All, First, Count, Exists
Section titled “Terminals: All, First, Count, Exists”.All(ctx) returns []T. Three more terminals round out the common reads:
.First(ctx)returns the first matching row, orliteorm.ErrNoRowsif there are none. It appliesLIMIT 1for you..Count(ctx)returns the matching row count asint64, ignoring order/limit/offset..Exists(ctx)returns abool.
cheapest, err := query.Select[Product](db). Filter(query.Col[int64]("stock").Gt(0)). OrderBy("price"). First(ctx)if errors.Is(err, liteorm.ErrNoRows) { // nothing in stock}
inStock, _ := query.Select[Product](db).Filter(query.Col[int64]("stock").Gt(0)).Count(ctx)anyInactive, _ := query.Select[Product](db).Filter(query.Col[bool]("active").Eq(false)).Exists(ctx)For a large result set you’d rather not hold in memory, .Iter(ctx) streams rows lazily — see streaming & aggregates.
See also
Section titled “See also”- Joins & subqueries — pulling in other tables and correlating with
IN/EXISTS. - Streaming & aggregates —
Iter,Pluck,GroupBy, and the aggregate terminals. - Writing data — inserts, upserts, and multi-row
Update/Delete. - Errors —
ErrNoRowsand the other normalized errors. - Transactions — running these reads inside a tx.