Skip to content

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.

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.

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.

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" <= ?

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.

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)

.All(ctx) returns []T. Three more terminals round out the common reads:

  • .First(ctx) returns the first matching row, or liteorm.ErrNoRows if there are none. It applies LIMIT 1 for you.
  • .Count(ctx) returns the matching row count as int64, ignoring order/limit/offset.
  • .Exists(ctx) returns a bool.
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.