zanith

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

TS
import { lit, fieldRef } from 'zanith';
 
lit(42) // bound parameter — never interpolated
lit('hello') // ditto
fieldRef('User', 'email') // reference a model field outside a query callback

lit(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

TS
import { caseWhen, coalesce, nullIf, lit, fieldRef } from 'zanith';
 
// CASE WHEN posts.viewCount > 1000 THEN 'high' ELSE 'low' END
caseWhen(
[{ 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 COALESCE
nullIf(fieldRef('User', 'name'), lit(''));
HelperSQLUse when
caseWhen(branches, { else? })CASE WHEN … THEN … [ELSE …] ENDBranching 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.

TS
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 SQL
fn('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).

HelperPostgres 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

HelperPostgres operator
arrayContains(field, values)field @> ARRAY[...]
arrayContainedBy(field, values)field <@ ARRAY[...]
arrayOverlaps(field, values)field && ARRAY[...]
arrayEquals(field, values)field = ARRAY[...]
TS
import { arrayContains, lit, fieldRef } from 'zanith';
 
// tags @> ARRAY['rust', 'web'] — row's tags include both
arrayContains(fieldRef('Post', 'tags'), [lit('rust'), lit('web')]);

Range helpers

For Postgres range types — int4range, tstzrange, and friends.

HelperPostgres operator
rangeContains(range, value)range @> value
rangeContainedBy(range, other)range <@ other
rangeOverlaps(range, other)range && other
rangeAdjacent(range, other)range -|- other
TS
import { rangeContains, fieldRef, lit } from 'zanith';
 
// active_period @> NOW() — the row's tstzrange covers right now
rangeContains(fieldRef('Subscription', 'activePeriod'), fn('now', []));

Full-text search expression

TS
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.