Skip to main content
Version: Next

Functions

Durcno provides a suite of type-safe SQL functions that can be used in .select(), .where(), and .orderBy() clauses. Functions are strongly typed against your column definitions — string functions only accept string columns, numeric functions only accept numeric columns, and so on.

Functions can also be composed: you can pass the result of one function as the input to another (e.g., lower(trim(Users.email))), and the type system enforces that both types are compatible.

Aggregate Functions​

Aggregate functions collapse multiple rows into a single value. When you mix aggregate and non-aggregate expressions in a single .select() call, Durcno automatically generates the appropriate GROUP BY clause for the non-aggregate columns.

FunctionSQLReturnsDescription
count("*")count(*)numberCounts all rows including nulls
count(col)count(col)numberCounts non-null values in the column
countDistinct(col)count(DISTINCT col)numberCounts distinct non-null values in the column
sum(col)sum(col)ColType | nullSum of non-null values; type follows the column; null if no rows match
avg(col)avg(col)ColType | nullAverage value; type follows the column; null if empty
min(col)min(col)ColType | nullMinimum value; type follows the column
max(col)max(col)ColType | nullMaximum value; type follows the column
jsonAgg(expr)json_agg(expr)T[] | nullAggregates values or objects into a JSON array; null if 0 rows match
jsonbAgg(expr)jsonb_agg(expr)T[] | nullAggregates values or objects into a JSONB array; null if 0 rows match

count​

Counts rows. Pass "*" to count all rows (including nulls), or a column to count only non-null values.

import { count } from "durcno";

// Count all rows
const [{ total }] = await db.from(Users).select(() => ({ total: count("*") }));

// Count non-null values in a specific column
const [{ emailCount }] = await db
.from(Users)
.select(() => ({ emailCount: count(Users.email) }));

countDistinct​

Counts the number of distinct non-null values.

import { countDistinct } from "durcno";

const [{ uniqueTypes }] = await db
.from(Users)
.select(() => ({ uniqueTypes: countDistinct(Users.type) }));

sum​

Returns the sum of all non-null values. The return type follows the column's TypeScript type (e.g., bigint | null for bigint/bigserial columns). Returns null if no rows match.

import { sum } from "durcno";

const [{ total }] = await db
.from(Orders)
.select(() => ({ total: sum(Orders.amount) }));

avg​

Returns the average value. The return type follows the column's TypeScript type (e.g., number | null for numeric columns). Returns null if no rows match.

import { avg } from "durcno";

const [{ average }] = await db
.from(Orders)
.select(() => ({ average: avg(Orders.amount) }));
// average is `number | null`

min / max​

Returns the minimum or maximum value. The return type matches the column's value type (or null if no rows match).

import { max, min } from "durcno";

const [row] = await db.from(Orders).select(() => ({
earliest: min(Orders.createdAt),
latest: max(Orders.createdAt),
}));
// earliest and latest are `Date | null`

Aggregates with GROUP BY​

When mixing aggregate and non-aggregate columns in a single .select(), Durcno auto-generates the GROUP BY:

import { count, sum } from "durcno";

const stats = await db.from(Orders).select(() => ({
status: Orders.status,
total: count("*"),
revenue: sum(Orders.amount),
}));
// Equivalent to: SELECT status, count(*), sum(amount) FROM orders GROUP BY status

Aggregates with HAVING and orderBy​

Aggregate functions can be filtered in .having() clauses and sorted by alias in .orderBy():

import { count, desc, gt, sum } from "durcno";

// Order by aggregate alias
await db
.from(Orders)
.select(() => ({ status: Orders.status, total: count("*") }))
.orderBy((_, { total }) => desc(total));

// Filter by aggregate in HAVING
await db
.from(Orders)
.select(() => ({ status: Orders.status, total: count("*") }))
.having(() => gt(count("*"), 5));

Filter and OrderBy Modifiers (FILTER (WHERE ...) and ORDER BY)​

All aggregate functions inherit from AggregateSqlFn and support inline .filter() and .orderBy() modifiers:

import {
asc,
count,
desc,
eq,
gt,
jsonAgg,
jsonBuildObject,
sum,
} from "durcno";

const stats = await db.from(Orders).select(() => ({
// SQL: count(id) FILTER (WHERE status = 'completed')
completedCount: count(Orders.id).filter(eq(Orders.status, "completed")),

// SQL: sum(amount) FILTER (WHERE amount > 100)
largeOrdersTotal: sum(Orders.amount).filter(gt(Orders.amount, 100)),

// SQL: json_agg(json_build_object('id', id, 'amount', amount) ORDER BY amount DESC)
sortedOrders: jsonAgg(
jsonBuildObject({
id: Orders.id,
amount: Orders.amount,
}),
).orderBy(desc(Orders.amount)),
}));

String Functions​

String functions accept any string column (varchar, text, char) or the result of another string-producing function.

FunctionSQLReturnsDescription
length(expr)length(expr)numberNumber of characters in the string
lower(expr)lower(expr)stringConverts to lowercase
upper(expr)upper(expr)stringConverts to uppercase
trim(expr)trim(expr)stringRemoves leading and trailing whitespace
left(expr, n)left(expr, n)stringFirst n characters of the string
right(expr, n)right(expr, n)stringLast n characters of the string
position(expr, substr)strpos(expr, substr)number1-based position of substr; 0 if not found
concat(...exprs)concat(...)stringConcatenates text representations of args
concatWs(sep, ...exprs)concat_ws(...)stringConcatenates arguments with a separator

lower / upper​

Convert a string expression to lowercase or uppercase.

import { lower, upper } from "durcno";

const result = await db.from(Users).select(() => ({
emailLower: lower(Users.email),
nameUpper: upper(Users.name),
}));

trim​

Removes leading and trailing whitespace.

import { trim } from "durcno";

const result = await db.from(Users).select(() => ({
cleanName: trim(Users.name),
}));

length​

Returns the number of characters in a string expression.

import { gt, length } from "durcno";

// Select the length
const result = await db
.from(Users)
.select(() => ({ nameLength: length(Users.name) }));

// Filter by length
await db
.from(Users)
.select("*")
.where(() => gt(length(Users.name), 5));

left / right​

Return the first or last n characters of a string.

import { left, right } from "durcno";

const result = await db.from(Users).select(() => ({
prefix: left(Users.postalCode, 3),
suffix: right(Users.postalCode, 3),
}));

position​

Returns the 1-based position of a substring within a string expression. Returns 0 if not found.

import { gt, position } from "durcno";

// Get position of '@' in email
const result = await db.from(Users).select(() => ({
atPos: position(Users.email, "@"),
}));

// Filter emails where '@' appears after position 5
await db
.from(Users)
.select("*")
.where(() => gt(position(Users.email, "@"), 5));

concat​

Concatenates the text representations of all arguments into a single string. null arguments are ignored, matching PostgreSQL's concat() behavior. Arguments can be columns, SQL functions, raw sql expressions, prepared Args, or primitive values.

import { concat } from "durcno";

const result = await db.from(Users).select(() => ({
fullName: concat(Users.firstName, " ", Users.lastName),
identifier: concat(Users.username, "#", Users.id),
}));

concatWs​

Concatenates arguments with a separator string (concat_ws). If the separator is null, the result is null. Any null values among the expressions to concatenate are skipped.

import { concatWs } from "durcno";

const result = await db.from(Users).select(() => ({
address: concatWs(", ", Users.city, Users.state, Users.country),
}));

Composing String Functions​

String functions that return a string can be passed as the input to another string function:

import { lower, startsWith, trim } from "durcno";

// Trim whitespace then lowercase before filtering
await db
.from(Users)
.select("*")
.where(() => startsWith(lower(trim(Users.email)), "admin"));

Mathematical Functions​

Mathematical functions accept any numeric column (integer, bigint, numeric, smallint, etc.) or the result of another numeric-producing function.

FunctionSQLReturnsDescription
abs(expr)abs(expr)numberAbsolute value
mod(expr, n)mod(expr, n)numberRemainder of expr divided by n
round(expr)round(expr)numberRounds to the nearest integer
round(expr, n)round(expr, n)numberRounds to n decimal places
ceil(expr)ceil(expr)numberSmallest integer ≥ expression (round up)
floor(expr)floor(expr)numberLargest integer ≤ expression (round down)
trunc(expr)trunc(expr)numberTruncates to nearest integer
trunc(expr, n)trunc(expr, n)numberTruncates to n decimal places
power(expr, n)power(expr, n)numberRaises expr to the power of n

abs​

Returns the absolute value of a numeric expression.

import { abs } from "durcno";

const result = await db.from(Accounts).select(() => ({
absBalance: abs(Accounts.balance),
}));

mod​

Returns the remainder of dividing the expression by n.

import { eq, mod } from "durcno";

// Get rows with even IDs
await db
.from(Users)
.select("*")
.where(() => eq(mod(Users.id, 2), 0));

round​

Rounds a numeric expression. Omit decimals to round to the nearest integer, or pass a value to round to that many decimal places.

import { round } from "durcno";

const result = await db.from(Products).select(() => ({
roundedPrice: round(Products.price),
twoDecimals: round(Products.price, 2),
}));

ceil / floor​

Round up or down to the nearest integer.

import { ceil, floor } from "durcno";

const result = await db.from(Products).select(() => ({
ceiling: ceil(Products.price),
floored: floor(Products.price),
}));

trunc​

Truncates a numeric expression towards zero. Omit decimals to truncate to the nearest integer, or pass a value to truncate to that many decimal places.

import { trunc } from "durcno";

const result = await db.from(Products).select(() => ({
truncatedPrice: trunc(Products.price),
twoDecimals: trunc(Products.price, 2),
}));

power​

Returns a numeric expression raised to the power of exponent.

import { power } from "durcno";

const result = await db.from(Products).select(() => ({
squared: power(Products.price, 2),
}));

Arithmetic Operators​

Arithmetic operators combine two numeric expressions using standard math operators. Both operands can be a numeric column, the result of another numeric function, a plain number or bigint literal, an Arg, or a typed Sql expression (Sql<number> / Sql<bigint>).

FunctionSQLReturnsDescription
add(a, b)(a + b)numberSum of two numeric expressions
sub(a, b)(a - b)numberDifference of two numeric expressions
mul(a, b)(a * b)numberProduct of two numeric expressions
div(a, b)(a / b)numberQuotient of two numeric expressions

Basic Usage​

import { add, div, mul, sub } from "durcno";

const result = await db.from(Orders).select(() => ({
grossTotal: add(Orders.subtotal, Orders.tax),
discount: sub(Orders.price, Orders.discountAmount),
doubled: mul(Orders.quantity, 2),
half: div(Orders.amount, 2),
}));

Nesting Arithmetic Operators​

Arithmetic results are wrapped in parentheses, so they compose safely:

import { add, mul, sub } from "durcno";

// (age * 2) + (5 - 1)
const result = await db.from(Users).select(() => ({
derived: add(mul(Users.age, 2), sub(5, 1)),
}));

Conditional Functions​

Conditional functions evaluate expressions based on conditions or nullability. Operands can be columns, SQL functions, raw sql expressions, prepared Args, primitive literals (string, number, bigint, boolean), or null.

FunctionSQLReturnsDescription
coalesce(...exprs)coalesce(...)InferredReturns the first non-null argument; drops null if any argument is non-null
nullif(expr, val)nullif(expr, val)T | nullReturns null if expr = val, otherwise returns expr
greatest(...exprs)greatest(...)InferredReturns the largest value; skips nulls; drops null if any is non-null
least(...exprs)least(...)InferredReturns the smallest value; skips nulls; drops null if any is non-null
caseWhen(cond, res)CASE WHEN ... THEN ..InferredBuilds a type-safe SQL CASE expression with .when(), .else(), and .end()

coalesce​

Returns the first non-null expression among its arguments. Durcno evaluates argument nullability from left to right: if any argument is guaranteed non-null, null is automatically excluded from the inferred return type. When used with arrays (such as coalesce(jsonAgg(...), [])), coalesce narrows the type to a guaranteed non-null array T[].

import { coalesce } from "durcno";

// If fallback is non-null, result type is guaranteed non-null string
const result = await db.from(Users).select(() => ({
displayName: coalesce(Users.nickname, Users.username),
contactEmail: coalesce(
Users.alternateEmail,
Users.email,
"no-reply@example.com",
),
}));

caseWhen​

Constructs a type-safe CASE WHEN ... THEN ... ELSE ... END expression. Durcno infers the exact union (|) type across all branch cases, preserving string and numeric literals without widening them to broad primitives.

Both .else(...) and .end() are optional: in PostgreSQL, omitting the ELSE clause implicitly returns NULL when no conditions match, so Durcno infers Branches | null.

import { caseWhen, eq, isNull, jsonBuildObject } from "durcno";

// Inferred return type: "Administrator" | "Staff" | "Member"
const usersWithRole = await db.from(Users).select(() => ({
username: Users.username,
badge: caseWhen(eq(Users.type, "admin"), "Administrator")
.when(eq(Users.type, "moderator"), "Staff")
.else("Member"),
}));

// Direct caseWhen without .else() or .end() -> inferred as "Admin" | null
const adminsOnly = await db.from(Users).select(() => ({
username: Users.username,
role: caseWhen(eq(Users.type, "admin"), "Admin"),
}));

// Returning null on unmatched LEFT JOIN -> inferred as null | { id: bigint; username: string }
const postsWithAuthor = await db
.from(Posts)
.leftJoin(Users, () => eq(Users.id, Posts.userId))
.select(({ users }) => ({
title: Posts.title,
author: caseWhen(isNull(users.id), null).else(
jsonBuildObject({
id: users.id,
username: users.username,
}),
),
}));

nullif​

Returns null if expr equals val, otherwise returns expr. Useful for converting sentinel values or empty states into SQL NULL.

import { nullif } from "durcno";

const result = await db.from(Users).select(() => ({
status: nullif(Users.status, "unknown"),
}));
// status is string | null

greatest / least​

Returns the largest or smallest value among its arguments, skipping null values (unless all arguments are null). If any argument is guaranteed non-null, null is automatically excluded from the inferred return type.

import { greatest, least } from "durcno";

const result = await db.from(Users).select(() => ({
latestActivity: greatest(Users.updatedAt, Users.createdAt),
minScore: least(Users.examScore, Users.quizScore, 0),
}));

Subquery Functions​

Subquery functions implement PostgreSQL's subquery expressions (Section 9.24). They evaluate subquery returns or membership checks and yield a typed boolean value (true or false).

All subquery functions can be used in:

  • .select(...) projections as boolean columns
  • .where(...) and .having(...) clauses to filter rows
  • caseWhen(...) conditions and branch results
  • Compound boolean logic with and(...) and or(...)
FunctionSQLReturnsDescription
exists(subquery)EXISTS (...)booleanReturns true if subquery returns at least one row, else false
notExists(subquery)NOT EXISTS (...)booleanReturns true if subquery returns no rows, else false
isIn(col, valuesOrSubquery)col IN (...)booleanReturns true if column matches any value in list or subquery, else false
notIn(col, valuesOrSubquery)col NOT IN (...)booleanReturns true if column matches no value in list or subquery, else false

exists / notExists​

Evaluates whether a subquery returns any rows. Correlated subqueries can reference columns from the outer query scope:

import { eq, exists, notExists } from "durcno";

// Project existence as boolean columns in .select()
const userProfiles = await db.from(Users).select(() => ({
id: Users.id,
username: Users.username,
hasPosts: exists(
db
.from(Posts)
.select("*")
.where(() => eq(Posts.userId, Users.id)),
),
isNewUser: notExists(
db
.from(Posts)
.select("*")
.where(() => eq(Posts.userId, Users.id)),
),
}));

// Use in WHERE clause (correlated subquery)
const authors = await db
.from(Users)
.select("*")
.where(() =>
exists(
db
.from(Posts)
.select("*")
.where(() => eq(Posts.userId, Users.id)),
)
);

isIn / notIn​

Checks whether a column value matches any (or none) of the values in a literal array or subquery result. Unlike basic filters, isIn and notIn can also be projected directly in .select():

import { isIn, notIn } from "durcno";

// Project membership check as boolean columns in .select()
const usersWithFlags = await db.from(Users).select(() => ({
id: Users.id,
isAdmin: isIn(Users.type, ["admin", "superadmin"]),
isNotBanned: notIn(Users.status, ["banned", "suspended"]),
}));

// In WHERE clause with value array
const activeAdmins = await db
.from(Users)
.select("*")
.where(() => isIn(Users.type, ["admin", "moderator"]));

// In WHERE clause with subquery
const usersWithRecentOrders = await db
.from(Users)
.select("*")
.where(() =>
isIn(
Users.id,
db.from(Orders).select(() => ({ userId: Orders.userId })),
)
);

// In WHERE clause with NOT IN subquery
const usersWithoutOrders = await db
.from(Users)
.select("*")
.where(() =>
notIn(
Users.id,
db.from(Orders).select(() => ({ userId: Orders.userId })),
)
);

When an empty array is passed:

  • isIn(col, []) automatically evaluates to FALSE in SQL.
  • notIn(col, []) automatically evaluates to TRUE in SQL.

JSON & JSONB Functions​

Durcno provides full type inference and runtime safety for PostgreSQL JSON and JSONB constructors and aggregates:

FunctionSQLDescription
jsonBuildObjectjson_build_object(k1, v1, ...)Builds a typed JSON object from key-value pairs
jsonbBuildObjectjsonb_build_object(k1, v1, ...)Builds a typed JSONB object from key-value pairs
jsonAggjson_agg(expr)Aggregates rows or objects into a typed JSON array
jsonbAggjsonb_agg(expr)Aggregates rows or objects into a typed JSONB array
toJsonto_json(tableOrExpr)Converts a table view or expression into a JSON object
toJsonbto_jsonb(tableOrExpr)Converts a table view or expression into a JSONB object
jsonBuildArrayjson_build_array(e1, e2, ...)Constructs a typed JSON array from expressions
jsonbBuildArrayjsonb_build_array(e1, e2, ...)Constructs a typed JSONB array from expressions
jsonStripNullsjson_strip_nulls(jsonObj)Strips null values from a JSON object
jsonbStripNullsjsonb_strip_nulls(jsonbObj)Strips null values from a JSONB object

jsonBuildObject / jsonbBuildObject​

Builds a typed JSON object by passing an object of fields. Fields can be columns, functions, literals, or nested jsonBuildObject calls:

import { jsonBuildObject } from "durcno";

const rows = await db.from(Users).select(() => ({
userCard: jsonBuildObject({
id: Users.id,
username: Users.username,
profile: jsonBuildObject({
bio: Users.bio,
}),
}),
}));
// userCard is inferred as { id: bigint; username: string; profile: { bio: string | null } }

jsonAgg / jsonbAgg with coalesce​

Aggregates multiple rows into a typed JSON array. Supports .filter(condition), .orderBy(...), and .distinct():

import { asc, coalesce, isNotNull, jsonAgg, jsonBuildObject } from "durcno";

const postsWithComments = await db
.from(Posts)
.leftJoin(Comments, () => eq(Comments.postId, Posts.id))
.select(({ comments }) => ({
id: Posts.id,
comments: coalesce(
jsonAgg(
jsonBuildObject({
id: comments.id,
body: comments.body,
}),
)
.orderBy(asc(comments.id))
.filter(isNotNull(comments.id)),
[],
),
}));
// comments is inferred as { id: bigint; body: string | null }[] (never null!)

toJson / toJsonb​

Converts a table view or table definition into a JSON or JSONB representation:

import { toJson, toJsonb } from "durcno";

const rows = await db.from(Users).select(() => ({
userJson: toJson(Users),
userJsonb: toJsonb(Users),
}));

jsonBuildArray / jsonbBuildArray​

Constructs an array from arguments:

import { jsonBuildArray } from "durcno";

const rows = await db.from(Users).select(() => ({
tags: jsonBuildArray(Users.type, "verified", 1),
}));

jsonStripNulls / jsonbStripNulls​

Removes object fields that contain SQL NULL:

import { jsonBuildObject, jsonStripNulls } from "durcno";

const rows = await db.from(Users).select(() => ({
cleaned: jsonStripNulls(
jsonBuildObject({
username: Users.username,
email: Users.email,
}),
),
}));

Null Safety & Return Typing​

Durcno SQL functions accurately model PostgreSQL strict function nullability semantics in TypeScript:

  • Strict null: Passing null directly (e.g., lower(null)) infers null.
  • Nullable inputs: If an argument is a nullable column or expression, the return type is automatically inferred as T | null.
  • Guaranteed non-null: When all arguments are guaranteed non-null (notNull columns, literal constants), the return type is inferred as T.

Functions in orderBy​

All scalar functions can be used with asc() / desc() in .orderBy():

import { asc, desc, length, lower } from "durcno";

// Order by lowercased name
await db
.from(Users)
.select("*")
.orderBy(() => asc(lower(Users.name)));

// Order by name length descending
await db
.from(Users)
.select("*")
.orderBy(() => desc(length(Users.name)));