Quarry
Guides

Expressions and functions

Use the expression builder for params, type conversions, null checks, and ClickHouse functions.

The expression builder is the callback-based API you reach for when the simple (column, operator, value) form of where and friends is not enough.

You access it by passing a callback to select, where, having, etc.:

db.selectFrom("users as u").where((eb) => /* eb is the expression builder */);

This guide focuses on the patterns you reach for in real queries. For the exhaustive list of every helper, every signature, and every return type, see the ExpressionBuilder reference.

Explicit parameter types

By default Quarry infers the ClickHouse placeholder type from the JS value. When you need to control it — for example to force DateTime instead of plain String — use param(value, "Type").

import { param } from "@oorestisime/quarry";

const rows = await db
  .selectFrom("event_logs as e")
  .select("e.user_id", "e.created_at")
  .where("e.created_at", ">=", param("2025-01-01 00:00:00", "DateTime"))
  .execute();

The string "DateTime" here is a ClickHouse type name, not a TypeScript type. It is the type that ends up inside the {p0:DateTime} placeholder in the generated SQL.

Type conversion helpers

Common ClickHouse cast functions are exposed under eb.fn:

const rows = await db
  .selectFrom("event_logs as e")
  .selectExpr((eb) => [
    eb.fn.toInt32("e.user_id").as("user_id_i32"),
    eb.fn.toDate("e.created_at").as("event_date"),
    eb.fn.toDateTime64("e.created_at", 3).as("event_time_ms"),
    eb.fn.toString("e.user_id").as("user_id_text"),
  ])
  .where((eb) => eb.fn.toUInt32("e.user_id"), ">", 0)
  .execute();

The currently exposed cast helpers are:

HelperReturns
eb.fn.toInt32(value)number
eb.fn.toInt64(value)string
eb.fn.toUInt32(value)number
eb.fn.toUInt64(value)string
eb.fn.toFloat32(value)number
eb.fn.toFloat64(value)number
eb.fn.toDate(value)string
eb.fn.toDateTime(value)string
eb.fn.toDateTime64(value, p)string
eb.fn.toString(value)string
eb.fn.toDecimal64(value, s)number
eb.fn.toDecimal128(value, s)number

The result types reflect the runtime-honest typing story: 64-bit ints return as string because that is what @clickhouse/client actually returns.

Date and time helpers

Common ClickHouse date/time helpers are also exposed under eb.fn:

import { param } from "@oorestisime/quarry";

const rows = await 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.toStartOfWeek("e.created_at").as("week_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.dateSub("hour", 1, "e.created_at").as("minus_one_hour"),
    eb.fn.toYYYYMM("e.created_at").as("event_yyyymm"),
  ])
  .execute();

Available date/time helpers:

HelperDescription
eb.fn.now()Current query-time DateTime.
eb.fn.today()Current query-time Date.
eb.fn.toStartOfMonth(value)Bucket to the start of the month.
eb.fn.toStartOfWeek(value)Bucket to the start of the week.
eb.fn.toStartOfDay(value)Bucket to the start of the day.
eb.fn.toStartOfYear(value)Bucket to the start of the year.
eb.fn.formatDateTime(value, format)Format with ClickHouse's MySQL-style datetime placeholders.
eb.fn.dateDiff(unit, start, end)Difference between two dates/times. Returns string (Int64).
eb.fn.dateAdd(unit, amount, value)Add a relative offset without dropping to raw SQL.
eb.fn.dateSub(unit, amount, value)Subtract a relative offset.
eb.fn.toYYYYMM(value)Compact integer YYYYMM key.
eb.fn.toYYYYMMDD(value)Compact integer YYYYMMDD key.

Accepted units are nanosecond, microsecond, millisecond, second, minute, hour, day, week, month, quarter, and year. Uppercase forms such as "DAY" also work.

Quarry does not yet have first-class INTERVAL expression nodes. For now, prefer dateAdd(...) and dateSub(...); drop to raw SQL only when you need the literal ClickHouse INTERVAL ... form itself.

formatDateTime(...) intentionally takes a literal format string. For literal date/time arguments in dateDiff(...), use eb.val(param(...)) because bare strings are interpreted as column references in these APIs.

Null checks

ClickHouse NULL semantics are not the same as ANSI SQL when joins are involved (see Runtime semantics). Quarry forces you to be explicit by exposing dedicated helpers instead of null equality.

const withEmail = await db
  .selectFrom("users as u")
  .select("u.id", "u.email")
  .whereNotNull("u.email")
  .execute();

const withoutEmail = await db
  .selectFrom("users as u")
  .select("u.id", "u.email")
  .whereNull("u.email")
  .execute();

Combining conditions

When you need multiple conditions combined with AND or OR inside a where(...) or having(...), use the callback form with eb.cmp(...) and eb.and([...]) / eb.or([...]).

db
  .selectFrom("users as u")
  .where((eb) =>
    eb.and([
      eb.cmp("u.email", "!=", ""),
      eb.cmp("u.status", "in", ["active", "pending"]),
      eb.or([
        eb.fn.has("u.tags", "premium"),
        eb.cmp(eb.fn.length("u.tags"), ">", 5),
      ]),
    ]),
  );

eb.cmp(left, operator, right) is the expression-level counterpart of the top-level where(column, operator, value) form. The left side can be a column reference or another expression (including an eb.fn.* call), and the right side accepts values, param(...), expressions, and subqueries.

For comparing two columns directly (both sides are refs), use eb.cmpRef(left, operator, right) instead — it is the narrower sibling.

Array helpers

const rows = await db
  .selectFrom("users as u")
  .select("u.id", "u.email")
  .where((eb) => eb.fn.has("u.tags", "premium"))
  .execute();
const rows = await db
  .selectFrom("users as u")
  .selectExpr((eb) => ["u.id", eb.fn.length("u.tags").as("tag_count")])
  .where((eb) => eb.fn.hasAny("u.tags", ["premium", "trial"]))
  .where((eb) => eb.fn.length("u.tags"), ">", param(0, "UInt64"))
  .execute();

Currently available array helpers:

HelperDescription
eb.fn.has(array, element)element is present in array
eb.fn.hasAny(array, elements)any of elements is present in array
eb.fn.hasAll(array, elements)all of elements are present in array
eb.fn.length(array)length of the array, returned as string
eb.fn.empty(value)1 if value is empty. Works on arrays and strings.
eb.fn.notEmpty(value)1 if value is non-empty. Works on arrays and strings.

length() returns ClickHouse UInt64, so under JSONEachRow it comes back as string at runtime. See Runtime semantics for why.

empty(...) and notEmpty(...) are deliberately broader than the rest of this section — they accept any "emptyable" input. That includes arrays, non-null strings, and nullable strings, so the same helper works whether you are checking that a tag list has entries or that an email field is set.

String helpers

const rows = await 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"),
  ])
  .execute();

Available string helpers:

HelperDescription
eb.fn.like(value, pattern)ClickHouse LIKE with % and _ wildcards.
eb.fn.ilike(value, pattern)Case-insensitive LIKE.
eb.fn.concat(part1, part2, ...rest)String concatenation. At least two parts required.
eb.fn.lower(value)Lowercase.
eb.fn.upper(value)Uppercase.
eb.fn.substring(value, offset, length)Slice from offset for length characters.
eb.fn.trimBoth(value)Strip whitespace from both ends.
eb.fn.trimLeft(value)Strip leading whitespace.
eb.fn.trimRight(value)Strip trailing whitespace.

The pattern argument for like / ilike and the parts of concat accept a literal JS string, a param(...), or another expression — you do not need to wrap them in eb.val(...) yourself. ClickHouse string predicates return 0/1, hence the number return type.

Null helpers

const rows = await db
  .selectFrom("users as u")
  .selectExpr((eb) => [
    "u.id",
    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"),
  ])
  .execute();

Available null helpers:

HelperDescription
eb.fn.isNull(value)1 if value is NULL, 0 otherwise.
eb.fn.isNotNull(value)1 if value is not NULL, 0 otherwise.
eb.fn.nullIf(value, nullValue)Returns NULL when value = nullValue.
eb.fn.coalesce(v1, v2, ...rest)Returns the first non-NULL expression. At least two args.
eb.fn.ifNull(value, defaultValue)Replaces NULL with defaultValue.

coalesce(...) is the one helper here that is expression-oriented: bare strings are treated as column references. If you want a literal fallback, wrap it with eb.val(...).

JSON helpers

const rows = await db
  .selectFrom("event_logs as e")
  .selectExpr((eb) => [
    "e.user_id",
    eb.fn.jsonExtractString("e.properties", "campaign").as("campaign"),
  ])
  .execute();

Aggregates and the -If combinator

The aggregate surface covers the common statistical and set helpers you reach for in analytics queries: count, sum, avg, min, max, uniq, uniqExact, groupArray, any, anyLast, plus their -If combinators. See the ExpressionBuilder reference for the full list and return types.

const stats = await db
  .selectFrom("event_logs as e")
  .selectExpr((eb) => [
    "e.user_id",
    eb.fn.count().as("total"),
    eb.fn.sum("e.amount").as("total_amount"),
    eb.fn.avg("e.amount").as("avg_amount"),
    eb.fn.uniqExact("e.session_id").as("unique_sessions"),
  ])
  .groupBy("e.user_id")
  .execute();

The -If combinator

ClickHouse's -If combinator takes an aggregate and restricts it to rows where an extra condition is truthy. It is one of the features that makes ClickHouse aggregations dramatically more expressive than standard SQL.

Quarry exposes typed helpers for the common -If variants: countIf, sumIf, avgIf, and uniqIf. The second argument is an expression — typically an eb.cmp(...) call.

const conversion = 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.countIf(eb.cmp("e.event_type", "=", "purchase")).as("purchases"),
    eb.fn
      .sumIf("e.amount", eb.cmp("e.event_type", "=", "purchase"))
      .as("revenue"),
  ])
  .groupBy("e.user_id")
  .execute();

One query gives you total events, signup count, purchase count, and purchase revenue — all computed in a single pass. In standard SQL you would need CASE WHEN ... END expressions inside each aggregate; ClickHouse (and Quarry) lets you write it directly.

On this page