Expression builders
The standalone constructors for Expression nodes. Use them anywhere a where, orderBy, select, or projection accepts an expression — including via _raw on the typed where surface.
Primitives — lit and fieldRef
import { lit, fieldRef } from 'zanith'; lit(42) // bound parameter — never interpolatedlit('hello') // dittofieldRef('User', 'email') // reference a model field outside a query callbacklit(value) is the safe way to inject a literal. It always becomes a bound parameter — there is no path through these helpers that interpolates values into the SQL string. fieldRefis for the cases where you need to reference a field but aren't inside a typed .where(({ user }) => ...) callback.
Control flow — caseWhen, coalesce, nullIf
import { caseWhen, coalesce, nullIf, lit, fieldRef } from 'zanith'; // CASE WHEN posts.viewCount > 1000 THEN 'high' ELSE 'low' ENDcaseWhen( [{ when: post.viewCount.gt(1000), then: lit('high') }], { else: lit('low') },); // COALESCE(nickname, email)coalesce([fieldRef('User', 'nickname'), fieldRef('User', 'email')]); // NULLIF(name, '') — turn empty strings into NULL for downstream COALESCEnullIf(fieldRef('User', 'name'), lit(''));| Helper | SQL | Use when |
|---|---|---|
caseWhen(branches, { else? }) | CASE WHEN … THEN … [ELSE …] END | Branching logic in projection or order key. |
coalesce([a, b, c]) | COALESCE(a, b, c) | First non-null wins. Often paired with nullIf to treat empty strings as missing. |
nullIf(a, b) | NULLIF(a, b) | Return NULL when the two are equal. Useful before coalesce. |
Function escape hatch — fn
fn(name, args)is the escape hatch for any SQL function the engine doesn't wrap. Every argument is an Expression and gets compiled through the same parameter-binding path as the rest of the query.
import { fn, fieldRef, lit } from 'zanith'; // LOWER(email)fn('LOWER', [fieldRef('User', 'email')]); // SUBSTRING(name FROM 1 FOR 3) — note: SUBSTRING's syntax is special; for that one drop to raw SQLfn('REPEAT', [fieldRef('User', 'name'), lit(2)]);JSON helpers — standalone form
The chained .jsonText(...) form on field references covers most cases — see JSONB. The standalone helpers are for places where you already have an Expression in hand (a fn(...) call result, a parameter, or a field referenced via fieldRef).
| Helper | Postgres operator |
|---|---|
jsonGet(field, key) | field->'key' |
jsonGetText(field, key) | field->>'key' |
jsonGetDeep(field, ['a','b','c']) | field#>'{a,b,c}' |
jsonGetDeepText(field, ['a','b','c']) | field#>>'{a,b,c}' |
jsonContains(field, value) | field @> value |
jsonContainedBy(field, value) | field <@ value |
jsonHasKey(field, key) | field ? key |
jsonHasAllKeys(field, keys) | field ?& keys |
jsonHasAnyKey(field, keys) | field ?| keys |
Array helpers
| Helper | Postgres operator |
|---|---|
arrayContains(field, values) | field @> ARRAY[...] |
arrayContainedBy(field, values) | field <@ ARRAY[...] |
arrayOverlaps(field, values) | field && ARRAY[...] |
arrayEquals(field, values) | field = ARRAY[...] |
import { arrayContains, lit, fieldRef } from 'zanith'; // tags @> ARRAY['rust', 'web'] — row's tags include botharrayContains(fieldRef('Post', 'tags'), [lit('rust'), lit('web')]);Range helpers
For Postgres range types — int4range, tstzrange, and friends.
| Helper | Postgres operator |
|---|---|
rangeContains(range, value) | range @> value |
rangeContainedBy(range, other) | range <@ other |
rangeOverlaps(range, other) | range && other |
rangeAdjacent(range, other) | range -|- other |
import { rangeContains, fieldRef, lit } from 'zanith'; // active_period @> NOW() — the row's tstzrange covers right nowrangeContains(fieldRef('Subscription', 'activePeriod'), fn('now', []));Full-text search expression
import { textSearch, fieldRef, lit } from 'zanith'; textSearch(fieldRef('Post', 'body'), lit('rust web'), { language: 'english', parser: 'websearch', // accept user-style queries ("foo bar" -baz)});For most search workloads the higher-level search() builder is the right tool — it composes text + trigram + vector ranking in one query. textSearch is the primitive when you want one text predicate inside an otherwise-normal relational query.