ExpressionBuilder
Reference for the expression builder, including every eb.fn helper.
The expression builder is the API you reach for when the simple
(column, operator, value) form of where, having, etc. is not enough.
You access it through callbacks:
db.selectFrom("users as u").where((eb) => /* eb is the expression builder */);
db.selectFrom("users as u").selectExpr((eb) => [/* projections */]);The builder is parameterised by the scope of the surrounding query, so all column references and function arguments are type-checked against the columns currently in scope.
Building expressions
eb.ref(columnRef)
Wraps a column reference as an Expression. The result type matches the
column's TypeScript type from the schema.
db
.selectFrom("users as u")
.selectExpr((eb) => [eb.ref("u.email")]);In most places where you can pass eb.ref("u.email") you can also pass the
plain string "u.email" directly. ref is useful when you specifically
need an Expression<T> value (e.g. to pass into a function helper that
takes either a column or an expression).
eb.val(value)
Wraps a JavaScript value as a typed parameterised expression. The compiled
SQL gets a {pN:Type} placeholder; the value is sent through the driver's
parameter map, not string-interpolated.
import { param } from "@oorestisime/quarry";
db
.selectFrom("users as u")
.where((eb) =>
eb.cmpRef("u.id", "=", "u.id"), /* placeholder */
)
.selectExpr((eb) => [eb.val(param(42, "UInt32"))]);For literal values where Quarry can infer the type, you can pass the JS
value directly to most APIs without going through eb.val.
eb.raw(sql)
Inserts raw SQL into the expression. No escaping is performed, so this is an explicit escape hatch — only use it for trusted, hard-coded SQL fragments.
db
.selectFrom("users as u")
.selectExpr((eb) => [eb.raw<string>("toString(u.id)").as("id_text")]);The optional type parameter (<string> above) controls how the result type
is treated downstream.
Comparing values
Quarry exposes two comparison helpers on the expression builder.
eb.cmp(...) is the general-purpose one you reach for most of the time.
eb.cmpRef(...) is a narrower variant for comparing two columns directly.
eb.cmp(left, operator, right)
The callback-form counterpart of
where(column, operator, value).
Use it inside eb.and([...]) or eb.or([...]) to combine multi-part
conditions, or when the left side needs to be an expression rather than a
raw column reference.
The left side accepts either a column reference or another Expression.
The right side accepts a scalar value, a param(...), another
Expression, or a subquery.
db
.selectFrom("users as u")
.where((eb) =>
eb.and([
eb.cmp("u.email", "!=", ""),
eb.cmp("u.status", "in", ["active", "pending"]),
eb.cmp(eb.fn.length("u.tags"), ">", 0),
]),
);Comparing an expression against a subquery:
const activeIds = db
.selectFrom("event_logs as e")
.select("e.user_id")
.where("e.event_type", "=", "signup");
db
.selectFrom("users as u")
.where((eb) => eb.cmp("u.id", "in", activeIds));Operators allowed: =, !=, >, >=, <, <=, in, not in. When the
operator is in / not in, the right side must be an array, a param
wrapping an array, or a subquery.
eb.cmpRef(left, operator, right)
Compares two column references. Both must resolve to columns in the current
scope. Use this when both sides are columns — for everything else use
eb.cmp.
db
.selectFrom("users as a")
.innerJoin("users as b", (eb) =>
eb.and([
eb.cmpRef("a.id", "=", "b.id"),
eb.cmpRef("a.email", "=", "b.email"),
]),
);Operators allowed for cmpRef are restricted to equality and ordering
(=, !=, <, <=, >, >=).
Combining expressions
eb.and(expressions)
Wraps an array of expressions in a logical AND.
db
.selectFrom("users as u")
.where((eb) =>
eb.and([
eb.fn.has("u.tags", "premium"),
eb.fn.notEmpty("u.email"),
]),
);eb.or(expressions)
Same shape, logical OR.
db
.selectFrom("users as u")
.where((eb) =>
eb.or([
eb.fn.has("u.tags", "premium"),
eb.fn.has("u.tags", "trial"),
]),
);and and or can be nested freely:
db
.selectFrom("users as u")
.where((eb) =>
eb.and([
eb.fn.notEmpty("u.email"),
eb.or([
eb.fn.has("u.tags", "premium"),
eb.fn.has("u.tags", "trial"),
]),
]),
);Functions: eb.fn
Every helper under eb.fn.* returns an Expression<T> where T is the
runtime TypeScript type for the value, not the ClickHouse type. See
runtime semantics for why some 64-bit
types come back as string.
You can chain .as("alias") on any Expression to give it a name in the
projection.
Aggregates
Every aggregate returns an Expression<T> where T follows the
runtime-honest typing rules: 64-bit
integer results (count, uniq, sum of UInt64) come back as string;
avg always returns number because ClickHouse produces Float64 for it;
min / max / any / anyLast preserve the input type; groupArray
preserves the input type except that NULL elements are dropped from nullable
inputs.
Count and numeric
| Helper | Returns | Notes |
|---|---|---|
eb.fn.count() | Expression<string> | count(*). UInt64 → string. |
eb.fn.countIf(condition) | Expression<string> | Counts only rows where condition is truthy. |
eb.fn.sum(value) | Expression<number | string> | Returns string for 64-bit integer columns, number otherwise. |
eb.fn.sumIf(value, condition) | Expression<number | string> | Sums only rows where condition is truthy. |
eb.fn.avg(value) | Expression<number> | Always returns number (ClickHouse produces Float64). |
eb.fn.avgIf(value, condition) | Expression<number> | Average over rows where condition is truthy. |
eb.fn.min(value) | Expression<T> | Minimum. Return type matches the input column type. |
eb.fn.max(value) | Expression<T> | Maximum. Return type matches the input column type. |
Distinct
| Helper | Returns | Notes |
|---|---|---|
eb.fn.uniq(value) | Expression<string> | Approximate distinct count (HyperLogLog). UInt64 → string. |
eb.fn.uniqExact(value) | Expression<string> | Exact distinct count. Slower than uniq. |
eb.fn.uniqIf(value, condition) | Expression<string> | Approximate distinct count over filtered rows. |
Collection and first-value
| Helper | Returns | Notes |
|---|---|---|
eb.fn.groupArray(value) | Expression<T[]> | Collects all values in the group into an array. For nullable inputs, NULL values are dropped, so Nullable(T) becomes T[]. |
eb.fn.any(value) | Expression<T> | Returns any value from the group. Nondeterministic which one. |
eb.fn.anyLast(value) | Expression<T> | Returns the last value seen in the group in insertion order. |
Example: combining aggregates
import { param } from "@oorestisime/quarry";
const stats = await db
.selectFrom("event_logs as e")
.selectExpr((eb) => [
"e.user_id",
eb.fn.count().as("total_events"),
eb.fn.countIf(eb.cmp("e.event_type", "=", "signup")).as("signups"),
eb.fn.sum("e.amount").as("total_amount"),
eb.fn
.avgIf("e.amount", eb.cmp("e.event_type", "=", "purchase"))
.as("avg_purchase"),
eb.fn.uniqExact("e.session_id").as("unique_sessions"),
eb.fn.groupArray("e.event_type").as("all_event_types"),
])
.groupBy("e.user_id")
.having("total_events", ">", param(0, "UInt64"))
.execute();The -If combinator pattern. ClickHouse supports an -If combinator on
most aggregates: sumIf, avgIf, countIf, uniqIf, and so on. The -If
variant takes an extra condition and only includes rows where that condition
is truthy. One query can then return, say, count(*), countIf(signup),
countIf(purchase), and sumIf(amount, purchase) in a single pass —
something standard SQL needs CASE WHEN ... END inside each aggregate to
approximate. Quarry exposes typed helpers for the most common -If
variants; for any you need that we do not yet wrap, you can fall through to
eb.raw(...).
Type conversions
These mirror the ClickHouse toX family. The result type follows the
runtime semantics: 64-bit ints come back
as string, decimals come back as number, dates come back as string.
| Helper | Returns | ClickHouse type produced |
|---|---|---|
eb.fn.toInt32(value) | Expression<number> | Int32 |
eb.fn.toInt64(value) | Expression<string> | Int64 |
eb.fn.toUInt32(value) | Expression<number> | UInt32 |
eb.fn.toUInt64(value) | Expression<string> | UInt64 |
eb.fn.toFloat32(value) | Expression<number> | Float32 |
eb.fn.toFloat64(value) | Expression<number> | Float64 |
eb.fn.toDate(value) | Expression<string> | Date |
eb.fn.toDateTime(value) | Expression<string> | DateTime |
eb.fn.toDateTime64(value, precision) | Expression<string> | DateTime64(precision) |
eb.fn.toString(value) | Expression<string> | String |
eb.fn.toDecimal64(value, scale) | Expression<number> | Decimal64(scale) |
eb.fn.toDecimal128(value, scale) | Expression<number> | Decimal128(scale) |
precision for toDateTime64 must be an integer between 0 and 9
(inclusive). scale for toDecimal64 must be 0..18, and for
toDecimal128 must be 0..38. Out-of-range values throw at build time.
db
.selectFrom("event_logs as e")
.selectExpr((eb) => [
eb.fn.toDate("e.created_at").as("event_date"),
eb.fn.toDateTime64("e.created_at", 3).as("event_time_ms"),
]);Date and time helpers
These helpers cover the common ClickHouse date/time patterns that show up in analytics queries: current timestamps, bucket boundaries, formatting, relative diffs, relative offsets, and compact integer date keys.
| Helper | Returns | Notes |
|---|---|---|
eb.fn.now() | Expression<string> | Current DateTime at query analysis time. |
eb.fn.today() | Expression<string> | Current Date. |
eb.fn.toStartOfMonth(value) | Expression<string> | Start of the month. |
eb.fn.toStartOfWeek(value) | Expression<string> | Start of the week. |
eb.fn.toStartOfDay(value) | Expression<string> | Start of the day. |
eb.fn.toStartOfYear(value) | Expression<string> | Start of the year. |
eb.fn.formatDateTime(value, format) | Expression<string> | format is a literal format string. |
eb.fn.dateDiff(unit, start, end) | Expression<string> | Returns ClickHouse Int64, so Quarry types it as string. |
eb.fn.dateAdd(unit, amount, value) | Expression<string> | Adds a unit-specific offset (day, hour, month, etc.). |
eb.fn.dateSub(unit, amount, value) | Expression<string> | Subtracts a unit-specific offset. |
eb.fn.toYYYYMM(value) | Expression<number> | Compact UInt32 year/month key. |
eb.fn.toYYYYMMDD(value) | Expression<number> | Compact UInt32 year/month/day key. |
Accepted units for dateDiff, dateAdd, and dateSub are:
nanosecond, microsecond, millisecond, second, minute, hour,
day, week, month, quarter, and year. Uppercase forms such as
"DAY" also work.
Quarry does not yet model ClickHouse INTERVAL as a first-class expression.
Use dateAdd(...) / dateSub(...) for typed relative offsets, or the raw
expression escape hatch when you specifically need literal INTERVAL ...
syntax.
If one side of dateDiff is a literal date/time rather than a column or
expression, wrap it with eb.val(param(...)) so Quarry does not treat the
string as a column reference.
import { param } from "@oorestisime/quarry";
db
.selectFrom("event_logs as e")
.selectExpr((eb) => [
eb.fn.now().as("query_time"),
eb.fn.today().as("query_date"),
eb.fn.toStartOfMonth("e.created_at").as("month_start"),
eb.fn.formatDateTime("e.created_at", "%Y-%m-%d").as("event_day"),
eb.fn
.dateDiff("day", eb.fn.toDate("e.created_at"), eb.val(param("2025-01-03", "Date")))
.as("days_until_cutoff"),
eb.fn.dateAdd("day", 7, "e.created_at").as("plus_week"),
eb.fn.toYYYYMM("e.created_at").as("event_yyyymm"),
]);JSON helpers
| Helper | Returns |
|---|---|
eb.fn.jsonExtractString(column, key) | Expression<string> |
db
.selectFrom("event_logs as e")
.selectExpr((eb) => [
eb.fn.jsonExtractString("e.properties", "campaign").as("campaign"),
]);The key argument is escaped for use as a single-quoted string literal.
More JSON helpers will be added — see the
roadmap.
Null helpers
| Helper | Returns | Notes |
|---|---|---|
eb.fn.isNull(value) | Expression<number> | Returns 1 when value is NULL, 0 otherwise. |
eb.fn.isNotNull(value) | Expression<number> | Returns 1 when value is not NULL, 0 otherwise. |
eb.fn.nullIf(value, nullValue) | Expression<T | null> | Returns NULL when value = nullValue; otherwise preserves the original type. |
eb.fn.coalesce(v1, v2, ...rest) | Expression<T> | Returns the first non-NULL expression. Accepts at least two expression inputs. |
eb.fn.ifNull(value, defaultValue) | Expression<T> | Replaces NULL with defaultValue, so the result is typed as non-null. |
isNull / isNotNull follow ClickHouse boolean semantics, so Quarry types
them as number (0 / 1).
coalesce(...) is expression-oriented: bare strings are treated as column
references, not string literals. Use eb.val(...) when you want a literal
fallback, and eb.val(param(...)) if that literal needs an explicit
ClickHouse type.
db
.selectFrom("users as u")
.selectExpr((eb) => [
eb.fn.nullIf("u.email", "").as("email_or_null"),
eb.fn.coalesce("u.nickname", "u.email").as("display_name"),
eb.fn.coalesce("u.nickname", eb.val("Unknown")).as("display_name_with_fallback"),
eb.fn.ifNull("u.nickname", "Unknown").as("nickname_or_default"),
]);Array helpers
| Helper | Returns | Notes |
|---|---|---|
eb.fn.has(array, element) | Expression<number> | 1 if element is in array, 0 otherwise. |
eb.fn.hasAny(array, elements) | Expression<number> | 1 if any of elements is in array. |
eb.fn.hasAll(array, elements) | Expression<number> | 1 if all of elements are in array. |
eb.fn.length(array) | Expression<string> | UInt64 → string. |
eb.fn.empty(value) | Expression<number> | Works on arrays and strings (returns 1 if empty). |
eb.fn.notEmpty(value) | Expression<number> | Works on arrays and strings. |
empty and notEmpty accept any "emptyable" value — arrays, strings,
or nullable strings. ClickHouse uses 0/1 for booleans, hence the
number return type.
db
.selectFrom("users as u")
.where((eb) => eb.fn.hasAny("u.tags", ["premium", "trial"]))
.selectExpr((eb) => [
"u.id",
eb.fn.length("u.tags").as("tag_count"),
]);String helpers
| Helper | Returns |
|---|---|
eb.fn.like(value, pattern) | Expression<number> |
eb.fn.ilike(value, pattern) | Expression<number> |
eb.fn.concat(part1, part2, ...rest) | Expression<string> |
eb.fn.lower(value) | Expression<string> |
eb.fn.upper(value) | Expression<string> |
eb.fn.substring(value, offset, length) | Expression<string> |
eb.fn.trimBoth(value) | Expression<string> |
eb.fn.trimLeft(value) | Expression<string> |
eb.fn.trimRight(value) | Expression<string> |
concat requires at least two parts. The pattern for like /
ilike follows the ClickHouse LIKE syntax (% and _ wildcards).
db
.selectFrom("users as u")
.where((eb) => eb.fn.ilike("u.email", "%@example.com"))
.selectExpr((eb) => [
"u.id",
eb.fn.lower("u.email").as("email_normalized"),
eb.fn.concat(eb.fn.upper("u.status"), " - ", "u.email").as("label"),
]);lower, upper, and the trim* helpers accept either a column reference
or a string-typed Expression. like, ilike, and substring accept the
same on the value side and ParamLike<string> on the pattern/length side
(so you can pass a literal string, a param(...), or another expression).
The Expression<T> and AliasedExpression<T, Alias> classes
eb.* always returns an Expression<T>. You can give it a name with
.as("alias"), which produces an AliasedExpression<T, Alias>. Both are
accepted by selectExpr and similar APIs:
const id = (eb: any) => eb.ref("u.id"); // Expression<number>
const idText = (eb: any) =>
eb.fn.toString("u.id").as("id_text"); // AliasedExpression<string, "id_text">In normal usage you do not need to construct these classes directly — they are returned to you by the builder.