Skip to main content

Functions

Gnok provides a comprehensive set of built-in functions for data transformation, analysis, and computation. Functions are categorized as scalar (operate on individual values), aggregate (summarize groups of rows), and window (compute over sliding frames).

All functions handle NULL inputs following SQL standard rules: most scalar functions return NULL if any argument is NULL, unless documented otherwise (e.g., coalesce, concat). NULL-handling forms (COALESCE, IFNULL, NVL) and CASE are evaluated per-row short-circuit — a later argument or branch is evaluated only on the rows that still need it, so defensive idioms like COALESCE(x, 1/0) do not crash on the dead branch.

Scalar Functions​

String Functions​

String manipulation and pattern matching. All string functions use 1-based indexing.

FunctionDescription
length(s) / len(s) / char_length(s)Character length
octet_length(s) / byte_length(s)Byte length (BYTE_LENGTH is the Trino spelling)
upper(s) / lower(s)Case conversion
initcap(s)Title-case each word
trim(s) / ltrim(s) / rtrim(s) / btrim(s)Whitespace trimming
lpad(s, len [, pad]) / rpad(s, len [, pad])Pad to length
substring(s, start [, len])Extract substring (1-indexed)
left(s, n) / right(s, n)First/last n characters
concat(s1, s2, ...)Concatenate strings, skipping NULL arguments (PG-style NULL-safe). When every argument is NULL the result is '' (the empty string), not NULL — this preserves the NULL-skip identity.
concat_ws(sep, s1, s2, ...)Concatenate with separator, skipping NULLs
replace(s, from, to)Replace all occurrences
translate(s, from, to)Character-by-character substitution
reverse(s)Reverse string
repeat(s, n) / replicate(s, n)Repeat string n times (REPLICATE is the T-SQL spelling)
position(sub IN s) / position(sub, s) / position(sub, s, start) / strpos(s, sub) / instr(s [, sub] [, start]) / instrb(s [, sub] [, start]) / charindex(sub, s [, start])Position of substring (1-indexed). The 2-arg call form POSITION(substring, string) takes the substring first. The 3-arg form takes a 1-based character start offset (multi-byte safe); start < 1 raises a bind error. CHARINDEX(needle, haystack [, start]) is the MSSQL spelling with reversed argument order vs STRPOS / INSTR — it lowers to INSTR(haystack, needle [, start]) so the optional start offset is honoured. INSTRB is an Oracle-compatible alias for INSTR.
elt(n, s1, s2, ...) / field(needle, s1, s2, ...)MySQL string-index idioms. ELT(n, ...) returns the n-th argument (1-indexed) or NULL if n is out of range; FIELD(needle, ...) returns the 1-based position of the first argument equal to needle, or 0 if none match. Both are rewritten to CASE at bind time so they work uniformly across distributed scans.
starts_with(s, prefix) / ends_with(s, suffix)Prefix/suffix test
contains(s, substr)Substring test
split_part(s, delimiter, n)Nth part after splitting
string_split(s, delimiter)Split into array
ascii(s) / codepoint(s) / chr(code) / nchar(code)First-character codepoint / character (CODEPOINT is the Trino / Presto spelling; NCHAR is the MSSQL Unicode-codepoint-to-char spelling and is an alias for CHR)
quote_literal(s) / quote_ident(s)PG SQL-safe quoting. QUOTE_LITERAL wraps s in single quotes and doubles any internal '; QUOTE_IDENT does the same with double quotes. NULL input → NULL. Useful for dynamically constructing SQL.
regexp_match(s, pattern) / regexp_like(s, pattern [, flags])Regex match (boolean). The optional 3rd flags argument follows the PG / Oracle convention — 'i' for case-insensitive, 's' for dot-matches-newline, 'm' for multi-line. Flags are spliced onto the pattern as an inline (?...) mode prefix.
s ~ pattern / s ~* pattern / s !~ pattern / s !~* patternPG POSIX regex operators. ~ matches; ~* matches case-insensitively; !~ / !~* are the negated forms. Lowered to REGEXP_LIKE(s, pattern) (with the 'i' flag for the * variants), wrapped in NOT for the bang variants.
regexp_extract(s, pattern [, group])Extract regex capture group
regexp_extract_all(s, pattern) / regexp_match_all(s, pattern)Extract all matches as ARRAY<VARCHAR> (empty array if none)
regexp_instr(s, pattern [, position])1-based byte offset of the first regex match, or 0 if there is no match. Optional position (1-based, default 1) is the byte offset to start searching from. Returns BIGINT.
regexp_replace(s, pattern, replacement)Regex replace. $N / ${N} / ${name} in replacement reference capture groups (Spark / Hive / PG ≥ 12). To emit a literal $, double it as $$.
sprintf(fmt, ...args) / printf(fmt, ...args) / format_string(fmt, ...args) / format(fmt, ...args)C / Trino / PG-style printf formatter. Supports %s / %d / %f / %x / %X / %o / %%, plus width, precision, and left-align flags. FORMAT(fmt, ...) disambiguates by first-arg type — a string fmt routes here, a numeric first arg routes to FORMAT_NUMBER.
regexp_split_to_array(s, pattern)Split on regex
levenshtein(s1, s2) / editdistance(s1, s2)Edit distance. NULL inputs (including a bare NULL literal) propagate row-wise — the row's result is NULL rather than a hard error. EDITDISTANCE is an alias.
jaro_winkler_similarity(s1, s2)Jaro-Winkler similarity (0–1)
jaccard(s1, s2)Jaccard similarity
url_encode(s) / url_decode(s)URL percent-encoding/decoding
-- Extract domain from email using regex capture group
SELECT regexp_extract(email, '@(.+)$', 1) AS domain FROM users;
-- 'gmail.com'

-- Split a path and get the filename
SELECT split_part('/data/warehouse/orders.parquet', '/', 4);
-- 'orders.parquet'

-- Fuzzy matching
SELECT name, levenshtein(name, 'Jhon Smith') AS distance
FROM customers
WHERE levenshtein(name, 'Jhon Smith') <= 2;

Numeric Functions​

Arithmetic, rounding, trigonometry, and bitwise operations.

FunctionDescription
abs(x)Absolute value
sign(x)Sign (-1, 0, 1) — the output type matches the input: sign(INTEGER) returns INTEGER, sign(DOUBLE) returns DOUBLE, sign(DECIMAL(p, s)) returns the same DECIMAL(p, s)
ceil(x) / floor(x)Rounding up/down
round(x [, d])Round to d decimal places
trunc(x [, d])Truncate toward zero
power(x, y)Exponentiation
sqrt(x) / cbrt(x)Square root / cube root
exp(x)e raised to the power x
ln(x) / log10(x) / log2(x)Natural / base-10 / base-2 logarithm
log(base, x)Logarithm of x with the given base
mod(x, y)Modulo
greatest(a, b, ...) / least(a, b, ...) / greatest_ignore_nulls(a, b, ...) / least_ignore_nulls(a, b, ...)Maximum / minimum of arguments. The _IGNORE_NULLS variants skip NULL arguments — they return NULL only when every argument is NULL. The bare forms propagate NULL on any NULL argument.
width_bucket(value, low, high, n_buckets)Equiwidth histogram bucket index for value over the half-open interval [low, high) divided into n_buckets equal-width buckets. Returns BIGINT: 0 for values below low, 1..n_buckets for in-range values, n_buckets + 1 for values at or above high. Descending bounds (low > high) are supported and reverse the bucket direction.
gcd(a, b) / lcm(a, b)Greatest common divisor / least common multiple
factorial(n)Factorial
pi()Returns π
sin(x) / cos(x) / tan(x)Trigonometric functions
asin(x) / acos(x) / atan(x) / atan2(y, x)Inverse trigonometric
radians(x) / degrees(x)Angle conversion
isnan(x) / isinf(x) / isfinite(x)Special value checks
bit_count(x) / popcount(x) / get_bit(x, pos) / bit_get(x, pos) / set_bit(x, pos, val)Bit operations (POPCOUNT is the Trino / Presto / DuckDB spelling; BIT_GET is the Spark spelling — alias for GET_BIT).
bitwise_and(x, y) / bitwise_or(x, y) / bitwise_xor(x, y) / bitwise_not(x) / bitshiftleft(x, n) / bitshiftright(x, n)Bitwise operators in function form. The infix operators & / | work for AND / OR. x ^ y (MySQL / Spark) and x # y (Postgres) lower to bitwise_xor. Note: << / >> infix shifts are accepted by the dialect for tokenisation but are not wired as binary operators today — use BITSHIFTLEFT(x, n) / BITSHIFTRIGHT(x, n) instead.

Domain errors​

SQRT, LN, LOG2, LOG10, and LOG(base, x) raise an explicit domain error on invalid inputs rather than returning silent NaN or -Infinity (which previously poisoned downstream SUM / AVG and comparisons). Matches Postgres semantics.

FunctionRejected input
SQRT(x)x < 0
LN(x) / LOG10(x) / LOG2(x)x <= 0
LOG(base, x)x <= 0, base <= 0, or base = 1

Use CASE WHEN x > 0 THEN LN(x) END (or a similar guard) if you specifically want NULL on out-of-domain values.

ROUND(decimal, n) narrows the declared scale​

ROUND(x, n) over a DECIMAL(p, s) input now returns a value whose declared scale is n, matching DuckDB / Postgres. Earlier the source's scale s was preserved, so the rounded value was right but trailing zeros leaked into CAST(... AS VARCHAR) and into the projected schema. Negative n (round to the nearest 10^|n|) narrows scale to 0:

SELECT ROUND(CAST(1.2345 AS DECIMAL(10, 4)), 2);            -- 1.23   (scale 2)
SELECT CAST(ROUND(CAST(1.2345 AS DECIMAL(10, 4)), 2)
AS VARCHAR); -- '1.23' (no trailing zeros)
SELECT ROUND(CAST(1234.5678 AS DECIMAL(10, 4)), -2); -- 1200 (scale 0)

DECIMAL input accepted by numeric / formatter / stats / spatial UDFs​

Because unquoted decimal literals (1.23, 100.00, 0.5) type as Decimal128(p, s) rather than Float64 (see Numeric Literal Typing), every UDF that takes a numeric argument accepts Decimal128 alongside Int* / Float*. The internal scale is unwound before the function sees the value, so callers don't need a defensive CAST(... AS DOUBLE) to invoke any of these on a decimal column or literal:

  • Formatters: FORMAT_NUMBER(x, n), TO_CHAR(x, fmt), SPRINTF / PRINTF / FORMAT(fmt, ...) with %f / %g, TO_VARCHAR, TO_STRING.
  • Stats UDFs: NORMAL_CDF(x), NORMAL_INV(p), T_TEST, T_TEST_P, CHI_SQUARE_TEST, CHI_SQUARE_TEST_P, MANN_WHITNEY, MANN_WHITNEY_P.
  • Spatial: H3_LATLNG_TO_CELL(lat, lng, res), S2_LATLNG_TO_CELL(lat, lng, level), ST_POINT(x, y) — all accept lat / lng / x / y as Decimal128 (e.g. 40.75, -73.98) without explicit cast.
  • Array constructors / set-ops: ARRAY_APPEND(arr, v) / ARRAY_PREPEND(v, arr) onto a List<Decimal128(p, s)> rescales an integer or wider-scale v to the list's (p, s); LIST_DISTINCT / ARRAY_DISTINCT / ARRAY_EXCEPT / ARRAY_INTERSECT / ARRAY_UNION over Decimal128 element lists preserve the inner type.
  • Time conversion: TO_TIMESTAMP(<decimal seconds>) / FROM_UNIXTIME(<decimal seconds>) interpret the fractional part as sub-second precision — TO_TIMESTAMP(1234567890.5) returns 2009-02-13 23:31:30.5 (earlier the Decimal128 literal was silently misread as microseconds-since-epoch and landed near 1970).

Where an aggregate that previously projected Float64 now sees a Decimal128 input, the output type follows the existing precision rules — see AVG(Decimal) / SUM(Decimal) widening.

Date/Time Functions​

Timestamp arithmetic, extraction, formatting, and bucketing. Timestamps are stored internally as microseconds since epoch (UTC).

FunctionDescription
now() / current_timestamp / sysdate / sysdate()Current timestamp. Bare SYSDATE (no parens) is the Oracle spelling and lowers to CURRENT_TIMESTAMP at bind time.
current_date / today()Current date
current_timeCurrent time
extract(unit FROM ts) / date_part(unit, ts)Extract field from timestamp
date_trunc(unit, ts)Truncate to unit
date_add(ts, interval) / date_sub(ts, interval)Add/subtract interval
date_diff(unit, ts1, ts2)Difference between timestamps
make_date(y, m, d)Construct date
make_time(h, m, s)Construct TIME from hour / minute / second. s may be Float64 / Float32 / DECIMAL for fractional seconds. Out-of-range hour / minute / second raises an explicit error (matches MAKE_TIMESTAMP) rather than silently returning NULL.
make_timestamp(y, m, d, h, min, s)Construct timestamp
dayofweekiso(date)ISO day-of-week (Mon = 1 … Sun = 7). Binder rewrite to EXTRACT(ISODOW FROM date).
strftime(format, ts)Format timestamp to string
strptime(s, format)Parse string to timestamp
date_format(ts, fmt)MySQL / Spark spelling — uses MySQL %-style codes (%Y year, %m month, %d day, %H 24-hour, %i minutes, %s seconds, %p AM/PM, %T = %H:%i:%s), not the YYYY-MM-DD masks TO_CHAR uses. DATE_FORMAT(ts, '%Y-%m-%d %H:%i:%s') → '2024-03-10 12:34:56'. NULL date or NULL format → NULL. For the PG YYYY-MM-DD mask dialect use TO_CHAR instead.
format_date(fmt, date)A spelling with arguments reversed vs STRFTIME — FORMAT_DATE('%Y-%m-%d', d) rewrites to STRFTIME('%Y-%m-%d', d) (strftime %-codes, not PG masks). Naive routing through TO_CHAR would silently return the literal %Y-%m-%d mask.
parse_date(fmt, s) / parse_timestamp(fmt, s) / parse_datetime(fmt, s)Date / timestamp parsers — arguments are reversed vs STRPTIME(s, fmt). The binder swaps the args, routes to STRPTIME, and casts to Date32 (PARSE_DATE) or Timestamp (PARSE_TIMESTAMP / PARSE_DATETIME). Format uses strftime %-codes.
to_date(s [, fmt]) / to_timestamp(s [, fmt])Parse using a PG-style format mask. TO_TIMESTAMP(numeric) is treated as epoch seconds and routes through FROM_UNIXTIME (not via CAST(<int> AS TIMESTAMP), which is rejected — see Rejected casts).
try_to_date(s [, fmt]) / try_to_timestamp(s [, fmt])Like to_date / to_timestamp but returns NULL on parse failure
from_unixtime(seconds)Convert Unix epoch seconds (BIGINT / DOUBLE) to TIMESTAMP. Use this (or TO_TIMESTAMP(seconds)) instead of CAST(<int> AS TIMESTAMP) — the cast is rejected to avoid the silent microseconds-vs-seconds unit confusion. FROM_UNIXTIME(NULL) propagates NULL (no longer errors "argument must be an integer or float").
epoch(ts) / unix_timestamp(ts)Seconds since Unix epoch. UNIX_TIMESTAMP(NULL) / EPOCH(NULL) propagate NULL. An unparseable string argument (e.g. UNIX_TIMESTAMP('garbage')) errors directionally rather than silently returning NULL — wrap with TRY_TO_TIMESTAMP(...) then EPOCH(...) if soft-failure is wanted.
epoch_ms(millis) / epoch_ns(nanos)Milliseconds/nanoseconds to timestamp
time_bucket(interval, ts)Truncate to time bucket (e.g., '5 minutes')
last_day(date)Last day of the month
-- Extract year and month
SELECT extract(YEAR FROM order_date) AS yr, extract(MONTH FROM order_date) AS mo
FROM orders;

-- Difference in days between two dates
SELECT date_diff('day', created_at, shipped_at) AS days_to_ship FROM orders;

-- `day` counts calendar-day boundary crossings, not 24h chunks of
-- elapsed time. This matches MSSQL and common SQL behavior.
SELECT date_diff('day', TIMESTAMP '2025-01-01 23:00',
TIMESTAMP '2025-01-02 01:00'); -- 1
-- Likewise `month` / `year` / `quarter` / `week` count boundary
-- crossings of the named unit. Use `hour` / `minute` / `second` /
-- `millisecond` / `microsecond` for true elapsed time.

-- 5-minute time buckets for metrics aggregation
SELECT time_bucket('5 minutes', event_time) AS bucket, COUNT(*)
FROM events
GROUP BY 1 ORDER BY 1;

-- Format for display
SELECT strftime('%Y-%m-%d %H:%M', order_date) AS formatted FROM orders;

-- Parse with a PG-style format mask (no chrono % codes required)
SELECT to_date('2025-03-14', 'YYYY-MM-DD');
SELECT to_timestamp('2025-03-14 09:30:15.123456', 'YYYY-MM-DD HH24:MI:SS.FF');

-- NULL inputs propagate cleanly (no panic)
SELECT to_date(NULL, 'YYYY-MM-DD'); -- NULL

EXTRACT return types​

EXTRACT (and the equivalent date_part) returns:

UnitReturn TypeNotes
EPOCHDOUBLEFractional seconds since Unix epoch — preserves microsecond precision
SECONDDOUBLEFractional seconds within the minute (e.g., 12.345678)
MILLISECOND / MICROSECONDBIGINTWhole-unit count
YEAR / QUARTER / MONTH / WEEK / DAY / DOW / DOY / HOUR / MINUTEBIGINTWhole-unit count

EPOCH and SECOND return DOUBLE so that downstream arithmetic preserves sub-second precision. Other units return BIGINT. If you were previously casting EXTRACT(EPOCH FROM ts) to a wider type defensively, you can drop the cast.

DATE_TRUNC units​

DATE_TRUNC(unit, ts) accepts the usual calendar units ('year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second') plus sub-second units 'millisecond' and 'microsecond' on TIMESTAMP inputs. The sub-second units floor the timestamp's microsecond field to the requested resolution (earlier builds silently returned the unmodified timestamp). Pre-1970 timestamps floor toward the past (div_euclid), matching Postgres.

SELECT DATE_TRUNC('millisecond', TIMESTAMP '2024-01-15 10:00:00.123456');
-- 2024-01-15 10:00:00.123000

SELECT DATE_TRUNC('microsecond', TIMESTAMP '2024-01-15 10:00:00.123456');
-- 2024-01-15 10:00:00.123456 (no-op at microsecond resolution)

TO_DATE / TO_TIMESTAMP format masks​

TO_DATE and TO_TIMESTAMP accept Postgres-style format masks and translate them to the underlying chrono format internally:

MaskMeaning
YYYY / YY4- / 2-digit year
MMZero-padded month
MON / MONTHAbbreviated / full month name
DDZero-padded day of month
HH24 / HH12Hour (24- or 12-hour)
MIMinute
SSSecond
FF / FF1–FF9Fractional seconds (1–9 digits)
AM / PMAM/PM marker
TZH / TZMTimezone hour / minute offset

TRY_TO_DATE / TRY_TO_TIMESTAMP return NULL on parse failure instead of raising. Both forms also handle NULL inputs cleanly — TO_DATE(NULL, 'YYYY-MM-DD') returns NULL.

TO_CHAR format masks​

TO_CHAR(ts, fmt) formats a DATE / TIMESTAMP to a VARCHAR using the same Postgres-style mask alphabet as TO_DATE / TO_TIMESTAMP. The fractional-second and timezone tokens below were corrected in Wave 97 — earlier builds either double-printed a leading . (e.g. ..123 for SS.FF3) or passed the token through as a literal.

MaskMeaning
FF / FF1–FF9Fractional seconds — n unprefixed digits. No leading . is emitted; include the dot in your mask explicitly (e.g. 'SS.FF3' → '12.123'). The default FF is 6 digits (microseconds, matching the storage resolution).
MS3-digit milliseconds, unprefixed (no leading .).
US6-digit microseconds, unprefixed (no leading .).
TZTimezone abbreviation. Renders empty for naive TIMESTAMP values, since the type carries no zone — cast to TIMESTAMPTZ first if you need a zone abbreviation.
OF / TZH:TZM / TZHNumeric UTC offset +HH:MM (e.g. +05:30, -08:00). TZH alone is rendered the same way — chrono has no native hours-only form.
QQuarter digit (1–4). Wired via a sentinel-substitution post-pass — chrono has no native Q directive but the substitution runs cleanly inside larger format masks.
-- Correct fractional-second formatting (user controls the dot)
SELECT TO_CHAR(TIMESTAMP '2024-01-15 10:30:45.123456',
'YYYY-MM-DD HH24:MI:SS.FF3');
-- '2024-01-15 10:30:45.123'

SELECT TO_CHAR(TIMESTAMP '2024-01-15 10:30:45.123456',
'YYYY-MM-DD"T"HH24:MI:SS.US');
-- '2024-01-15T10:30:45.123456' (US = 6 digits, no leading dot)
Known limitations
  • Case-folded day / month names (day / Day / DAY / mon / Mon) and double-quoted literal escapes (e.g. "T") are deferred — they need a post-format substitution pass that the current chrono-backed implementation doesn't run.

AT TIME ZONE​

ts AT TIME ZONE 'tz' shifts a timestamp between zone-naive and zone-aware representations. The right side must be a literal string naming an IANA zone ('America/New_York', 'UTC', 'Asia/Tokyo', 'Europe/Berlin', …); a column or expression on the right side is rejected at bind time.

There are two semantics depending on the input type:

InputBehaviourOutput type
Naive TIMESTAMPInterpret the wall-clock as local in tz, convert to UTCTimestamp(Microsecond, Some("UTC"))
Tz-aware TIMESTAMPTZ / Timestamp(_, Some(_))Preserve the instant; relabel for display in tzTimestamp(Microsecond, Some("UTC"))
-- Naive → tz-aware: "noon in New York" → the corresponding UTC instant
SELECT TIMESTAMP '2024-06-15 12:00:00' AT TIME ZONE 'America/New_York';
-- 2024-06-15T16:00:00Z (NYC noon in summer = UTC 16:00)

SELECT typeof(TIMESTAMP '2024-06-15 12:00:00' AT TIME ZONE 'America/New_York');
-- 'Timestamp(Microsecond, Some("UTC"))'

-- Tz-aware → tz-aware: the underlying instant is preserved
SELECT (TIMESTAMP '2024-06-15 12:00:00' AT TIME ZONE 'America/New_York')
AT TIME ZONE 'Asia/Tokyo';
-- 2024-06-16T01:00:00Z (same instant, displayed in Tokyo's frame)

AT TIME ZONE is the bridge between naive TIMESTAMP columns (timestamps without a zone) and TIMESTAMPTZ arithmetic. If your incoming column stores wall-clock values that are really "local in some zone", apply AT TIME ZONE '<their_zone>' before joining against a UTC-anchored series.

DATE + sub-day INTERVAL promotion​

Adding an INTERVAL with any sub-day component (hours, minutes, seconds) to a DATE automatically promotes the result to TIMESTAMP, matching Postgres. Pure-day intervals stay on the DATE path.

-- Result is TIMESTAMP '2024-01-15 01:30:00' (DATE was promoted)
SELECT DATE '2024-01-15' + INTERVAL '90 minutes';

-- Result stays DATE '2024-01-25' (no promotion)
SELECT DATE '2024-01-15' + INTERVAL '10 days';

Conditional Functions​

Control flow expressions for handling NULLs and branching logic.

FunctionDescription
CASE WHEN ... THEN ... ELSE ... ENDConditional expression (per-row short-circuit; THEN / ELSE branches are evaluated only on rows whose WHEN selected them)
coalesce(a, b, ...)First non-null value (per-row short-circuit)
nullif(a, b)Returns NULL if a = b
ifnull(a, b) / nvl(a, b)Returns b if a is NULL (per-row short-circuit, same semantics as coalesce(a, b))
is_null(x)Function-call form of the x IS NULL predicate (SQLite spelling). Returns BOOLEAN. The two-arg form ISNULL(x, y) is lowered to COALESCE(x, y) (T-SQL).
equal_null(a, b) / is_distinct_from(a, b)NULL-safe equality / inequality. EQUAL_NULL(a, b) lowers to a IS NOT DISTINCT FROM b — returns TRUE when both are NULL, FALSE when exactly one is NULL, and the usual a = b otherwise. IS_DISTINCT_FROM(a, b) is its negation (a IS DISTINCT FROM b).
booland(a, b) / boolor(a, b) / boolxor(a, b) / boolnot(x)Boolean function-call form of the logical operators. BOOLAND / BOOLOR / BOOLNOT map directly to AND / OR / NOT. BOOLXOR is decomposed as (a OR b) AND NOT (a AND b) so NULL propagates the standard way.

COALESCE, IFNULL, and NVL short-circuit per row: each subsequent argument is evaluated only on rows where every prior argument was NULL. This matches PostgreSQL / DuckDB / MySQL and lets you write the standard divide-by-zero defensive idiom:

-- Safe: the second argument is only evaluated on rows where qty IS NULL,
-- so the divide-by-zero on the dead branch never executes.
SELECT COALESCE(qty, 100 / 0) FROM orders;

COALESCE, GREATEST, LEAST, NVL, IFNULL, and NULLIF reject zero-argument calls at bind time. COALESCE / GREATEST / LEAST also promote Date + Timestamp mixes to Timestamp.

-- Categorize with CASE
SELECT order_id,
CASE
WHEN amount >= 1000 THEN 'high'
WHEN amount >= 100 THEN 'medium'
ELSE 'low'
END AS tier
FROM orders;

-- Default to 0 if NULL
SELECT coalesce(discount, 0) AS discount FROM orders;

-- Avoid division by zero (nullif returns NULL when denominator is 0)
SELECT total / nullif(count, 0) AS avg_per_item FROM summary;

Type Functions​

FunctionDescription
CAST(expr AS type) / TRY_CAST(expr AS type)Convert to type. CAST errors on failure; TRY_CAST returns NULL on failure. Both forms trim surrounding whitespace for numeric / temporal targets — TRY_CAST(' 42 ' AS INT) returns 42, matching CAST (earlier TRY_CAST returned NULL for whitespace-padded numerics, so defensive CAST→TRY_CAST swaps on CSV-style inputs silently nuked rows).
to_string(expr) / to_varchar(expr)Alias for CAST(expr AS VARCHAR). The 2-arg form TO_VARCHAR(date_or_ts, fmt) routes to TO_CHAR(date_or_ts, fmt).
typeof(expr)Return the underlying Arrow type name as a string

TYPEOF(expr) returns the runtime Arrow type — useful for debugging implicit-cast and type-inference behaviour. It mirrors DuckDB's typeof.

SELECT typeof(1);                           -- 'Int64'
SELECT typeof(1.5); -- 'Decimal128(2, 1)' (unquoted decimal literals are Decimal128)
SELECT typeof(1e5); -- 'Float64' (scientific notation stays float)
SELECT typeof('hello'); -- 'Utf8'
SELECT typeof(DATE '2025-01-15'); -- 'Date32'
SELECT typeof(TIMESTAMP '2025-01-15 10:00');-- 'Timestamp(Microsecond, None)'
SELECT typeof(CAST(1.5 AS DECIMAL(10, 2))); -- 'Decimal128(10, 2)'
SELECT typeof(ARRAY[1, 2, 3]); -- 'List(Int64)'
SELECT typeof(INTERVAL '1 day'); -- 'Interval(MonthDayNano)'

-- Decimal / Decimal stays in Decimal128 — the result scale is
-- max(6, left.scale), precision is widened to 38 to leave room for
-- the quotient. The reported `typeof` matches the actual column
-- type produced by the divide.
SELECT typeof(CAST(10 AS DECIMAL(18, 2)) / CAST(3 AS DECIMAL(18, 2)));
-- 'Decimal128(38, 6)'

-- Decimal / Int also produces Decimal128 — the integer is promoted
-- into Decimal first, the divide preserves the decimal track, and
-- `typeof` reports the actual runtime column type (earlier the
-- column was Decimal128 but TYPEOF returned 'Float64', confusing
-- downstream clients that switched code paths on the report).
SELECT typeof(CAST(10 AS DECIMAL(18, 2)) / CAST(3 AS INT));
-- 'Decimal128(38, 6)'

CASE-branch unification preserves decimal precision: a CASE expression whose branches are a mix of Decimal128 values unifies to a Decimal128 wide enough to hold every branch, rather than silently collapsing to Float64.

Hash and Encoding Functions​

FunctionDescription
md5(s)MD5 hash (32-char hex)
sha256(s)SHA-256 hash (64-char hex)
base64(s) / to_base64(s) / base64_encode(s) / encode_base64(s)Base64 encode — returns VARCHAR. The portable _ENCODE / ENCODE_* spellings are aliases for the bare form.
from_base64(s) / base64_decode(s) / decode_base64(s)Base64 decode — returns BINARY (wire layer renders raw bytes as \xHH… hex, consistent with UNHEX). The portable _DECODE / DECODE_* spellings are aliases.
hex(s) / to_hex(s) / hex_encode(s)Hex encode — returns VARCHAR. HEX_ENCODE is the portable spelling.
unhex(s) / from_hex(s) / hex_decode(s)Hex decode — returns BINARY. HEX_DECODE is the portable spelling.
encode(bytes, fmt) / decode(string, fmt)PG-style codec dispatch. fmt is one of 'base64', 'hex', or 'escape' (escape only on DECODE); the binder rewrites to the named encoder / decoder. ENCODE(b, 'hex') ≡ HEX(b); DECODE(s, 'base64') ≡ FROM_BASE64(s).
to_binary(s [, fmt]) / try_to_binary(s [, fmt])String-to-BINARY conversion. fmt is one of 'HEX' (default), 'BASE64', or 'UTF-8'. TO_BINARY errors on a malformed input; TRY_TO_BINARY returns NULL. Wire-layer rendering matches FROM_BASE64 / UNHEX — raw bytes as \xHH… over PG-wire, hex via the HTTP API.
try_to_double(s) / try_to_float(s) / try_to_real(s)NULL-on-failure floating-point parsers. Lowered to TRY_CAST(s AS DOUBLE) (the FLOAT / REAL spellings widen to Float64 to match TRY_CAST(... AS FLOAT)).
gen_random_uuid() / uuid_string() / random_uuid() / uuid() / uuidv4()Generate random UUID v4 (H2 / DuckDB / PG aliases)

JSON Functions​

Extract and inspect values from JSON-encoded VARCHAR columns. For native semi-structured data, see Variant Functions.

FunctionDescription
json_extract(json, path) / get_json_object(json, path) / get_path(variant, dotted_key)Extract value as JSON string (GET_JSON_OBJECT is the Hive / Spark spelling). GET_PATH(v, 'a.b') is a VARIANT dot-notation alias — the binder prepends '$.' to the second argument if missing so GET_PATH(v, 'a.b') ≡ JSON_EXTRACT(v, '$.a.b').
json_extract_string(json, path) / json_extract_scalar(json, path) / json_extract_path_text(json, path)Extract value as plain string (Trino / PG aliases)
json_exists(json, path)Path-existence test (SQL/JSON). Binder rewrites to JSON_EXTRACT(json, path) IS NOT NULL. Returns BOOLEAN.
json_array_length(json) / json_length(json)Length of JSON array
json_keys(json) / object_keys(json)Keys of JSON object (OBJECT_KEYS is an alias)
json_type(json)JSON type as a lowercase string: 'object', 'array', 'number', 'string', 'boolean', or 'null' (matches PG / DuckDB)
is_object(v) / is_array(v)VARIANT-introspection scalars: true if the JSON value at v is an object / array. Binder rewrite to JSON_TYPE(v) = 'object' / JSON_TYPE(v) = 'array'. NULL input propagates (because JSON_TYPE(NULL) is NULL and NULL = '…' is NULL).
json_valid(json)True if valid JSON
json_object(k1, v1, k2, v2, ...)Build a JSON object literal from key/value pairs (zero-arg form returns '{}')
json_array(v1, v2, ...)Build a JSON array literal (zero-arg form returns '[]')
json_quote(s)Quote and escape a value as a JSON string literal
-- Extract nested values (path uses $ notation)
SELECT json_extract(payload, '$.user.name') AS user_name,
json_extract_string(payload, '$.status') AS status
FROM events;

-- Iterate over a JSON array
SELECT json_array_length(json_extract(data, '$.items')) AS item_count
FROM orders;

Supported path syntax​

Gnok's JSON path is a strict subset of JSONPath: dot-notation field access ($.user.name) and bracketed numeric index access ($.items[0].name). Negative bracket indices are accepted and count from the end ($[-1] is the last element, matching DuckDB / MySQL).

Quoted member keys — for keys that contain ., [, or other path-syntax characters, both forms are accepted:

-- Dot-then-quoted
SELECT json_extract('{"a.b": 1}', '$."a.b"'); -- 1

-- Bracket-then-quoted (single or double quotes)
SELECT json_extract('{"a.b": 1}', '$["a.b"]'); -- 1
SELECT json_extract('{"a.b": 1}', E'$[\'a.b\']'); -- 1

The following constructs are not supported and raise an explicit error so users aren't silently handed NULL:

ConstructExampleStatus
Wildcard array index$.users[*].nameError
Recursive descent$..nameError
Filter expression$.users[?(@.age > 30)]Error

Use UNNEST over the JSON array (after a typed extract) for the wildcard / filter use cases.

Array Functions​

Operations on ARRAY (Iceberg list) columns. Use UNNEST to expand arrays into rows.

FunctionDescription
array_length(arr)Array length
array_contains(arr, val)Test membership
array_concat(arr1, arr2)Concatenate arrays
array_append(arr, val) / array_prepend(val, arr)Append / prepend a single element
array_position(arr, val) / array_index_of(arr, val) / index_of(arr, val)1-based index of val, or NULL if not found (Trino / Hive aliases)
array_sort(arr)Sort elements
array_distinct(arr)Remove duplicates
array_slice(arr, begin, end)Slice array with inclusive begin / end indices (PG / DuckDB semantics).
slice(arr, start, length)Spark (start, length) slice. Binder rewrite to ARRAY_SLICE(arr, start, start + length - 1). Negative start (offset-from-end) and length-overflow truncation are preserved through the underlying ARRAY_SLICE path.
arrays_overlap(a, b) / array_overlap(a, b)Spark "do these arrays share any element" boolean. Binder rewrite to ARRAY_LENGTH(ARRAY_INTERSECT(a, b)) > 0.
flatten(arr)Flatten nested array one level
generate_series(start, stop [, step]) / sequence(start, stop [, step])Generate integer series. SEQUENCE is the Spark alias and returns List(Int64) (the projection-schema return type is reported as List(Int64), not Utf8 — so Flight SQL / PG-wire clients decode the array natively rather than as a stringified list).
cardinality(arr)Size of array or map

|| operator on arrays​

The || operator concatenates arrays and now also handles array/scalar pairs without falling back to string-concatenation:

FormSemantics
`ARRAY
`ARRAY
`scalar
SELECT ARRAY[1, 2, 3] || 4;       -- [1, 2, 3, 4]
SELECT 0 || ARRAY[1, 2, 3]; -- [0, 1, 2, 3]
SELECT ARRAY[1, 2] || ARRAY[3]; -- [1, 2, 3]

array_position not-found semantics​

array_position(arr, x) returns NULL when x is not in arr, matching Postgres / DuckDB. Earlier builds returned 0 for not-found — if you have queries that test array_position(...) = 0, switch to array_position(...) IS NULL.

-- Generate a date series (useful for filling gaps in time-series data)
SELECT d::DATE AS day
FROM generate_series(1, 30) AS t(d);

-- Filter rows where the tags array contains 'urgent'
SELECT * FROM tickets WHERE array_contains(tags, 'urgent');

-- Slice first 3 elements (1-based, inclusive)
SELECT array_slice(items, 1, 3) FROM orders;

-- Spark (start, length) slice — rewritten to
-- ARRAY_SLICE(items, 2, 4) (inclusive end = start + length - 1).
SELECT slice(items, 2, 3) FROM orders;

-- Do two arrays share any element?
SELECT arrays_overlap(ARRAY[1, 2, 3], ARRAY[3, 4, 5]); -- true

Map Functions​

Operations on MAP (Iceberg map) columns — key-value collections with typed keys and values.

FunctionDescription
map_extract(map, key)Extract value by key
map_keys(map) / map_values(map)Keys / values as arrays
map_contains(map, key)Test key presence
map_entries(map)Key-value pairs as list of structs
map_from_entries(list)Construct map from entries

Struct Functions​

Operations on STRUCT (Iceberg struct) columns — named field collections.

FunctionDescription
struct_extract(s, field)Extract named field
struct_keys(s)Field names as array
struct_pack(v1, v2, ...) / struct(v1, v2, ...) / row(v1, v2, ...)Construct struct from positional values. STRUCT(v1, v2, ...) is the Spark alias and ROW(v1, v2, ...) is the standard-SQL spelling; both route to STRUCT_PACK. The named form STRUCT(v AS k) is not supported (sqlparser rejects it) — use STRUCT_PACK plus an outer AS rename, or NAMED_STRUCT / OBJECT_CONSTRUCT to build a JSON object instead.
named_struct(k1, v1, k2, v2, ...)Spark / Hive named-struct constructor. Routes to the same JSON-object dispatch as OBJECT_CONSTRUCT / JSON_OBJECT — keys must be string literals; values may be any expression. Returns a JSON object string.

Vector Distance Functions​

Compute distances between fixed-length float arrays for similarity search and ML applications. See Vector Operations for HNSW index support.

FunctionDescription
l2_distance(v1, v2)Euclidean (L2) distance
l1_distance(v1, v2)Manhattan (L1) distance
cosine_distance(v1, v2)Cosine distance (1 − similarity). COSINE_DISTANCE(v, v) is clamped to exactly 0.0 (pgvector / Faiss parity) so identical vectors compare equal and stable KNN ORDER BY doesn't break on a positive ULP.
inner_product(v1, v2) / inner_product_distance(v1, v2) / vector_inner_product_distance(v1, v2)Dot product (negative, for distance metric)
vector_norm(v)L2 norm of a single vector
hamming_distance(v1, v2)Hamming distance for binary embeddings
vector_avg(v)Element-wise average aggregate

All vector-distance scalars project as Float64 in the result schema, so ORDER BY <distance>(...) sorts numerically and downstream arithmetic (distance + 1.0, distance * weight) type-checks cleanly. Earlier builds shipped INNER_PRODUCT_DISTANCE, L1_DISTANCE, VECTOR_NORM, and VECTOR_INNER_PRODUCT_DISTANCE with a Utf8 projection-schema type — runtime produced Float64 but the wire layer string-formatted each row, so KNN ORDER BY ... LIMIT sorted lexicographically ('-10.0' < '-1.0' < '-0.0' < '5.0' as strings) and rankings were silently wrong.

pgvector distance operators​

Gnok also accepts the pgvector infix operators for similarity search, including against ARRAY[...] literals on either operand (earlier the operator rewriter only recognised (...) brackets, so an ARRAY literal on the left silently truncated the rewrite or left the right side as a bare identifier):

OperatorLowers toNotes
<->l2_distanceEuclidean distance
<=>cosine_distanceCosine distance
<#>inner_product (negated)Negative inner product
<~>hamming_distanceHamming distance for binary embeddings
-- ARRAY literal on either side now lowers cleanly
SELECT id FROM docs ORDER BY embedding <=> ARRAY[0.1, 0.2, ...]::FLOAT[]
LIMIT 10;

AI/ML Functions​

Scalar UDFs for in-query inference, embedding generation, and feature lookup. See AI/ML overview for the underlying primitives.

FunctionSignatureDescription
ML_PREDICT(model_name, ...args)(VARCHAR, …) -> <model return type>Invoke the ACTIVE version of a registered model. The first argument MUST be a string literal; remaining args are passed positionally to the model's input tensor. Works against ONNX / PyTorch / sklearn / native (KMeans, DNN, …) models uniformly. See ML_PREDICT.
EMBED(text)(VARCHAR) -> FixedSizeList<Float32, N>Generate an embedding via the configured embedding provider (managed by Gnok). Off until an organization administrator enables AI in Studio (Operations → AI Governance); counts against the organization's monthly token budget. The return type is FixedSizeList<Float32, N> where N is the provider's dimension (default 1536). Values render as JSON arrays through /api/query, as native list cells through Flight SQL, and feed directly into VECTOR(N) columns, COSINE_DISTANCE / L2_DISTANCE / DOT_PRODUCT, and the vector ANN index.
FEATURE_LOOKUP(group, key, feature)(VARCHAR, BIGINT, VARCHAR) -> FLOAT64Look up the named feature for key from the live snapshot of group. O(1) hash-indexed.
FEATURE_LOOKUP_AS_OF(group, key, feature, as_of_ms)(VARCHAR, BIGINT, VARCHAR, BIGINT) -> FLOAT64Point-in-time variant. Consults the in-memory snapshot ring first; falls through to the offline Iceberg table via time-travel on miss.
-- Inline inference + feature enrichment
SELECT t.transaction_id,
ML_PREDICT('fraud_detector',
t.amount,
FEATURE_LOOKUP('user_features', t.user_id, 'avg_amount'))
AS fraud_score
FROM transactions t
WHERE t.created_at > now() - INTERVAL '5 minutes';

-- Semantic search with inline embedding
SELECT id, title
FROM documents
ORDER BY COSINE_DISTANCE(embedding, EMBED('refund policy'))
LIMIT 10;

AI scalar LLM functions​

A comprehensive family of LLM-backed scalar UDFs that operate on text columns inline in any SELECT, WHERE, or JOIN expression. They share a single dispatch path with input deduplication, bounded parallel concurrency, per-call timeout, and exponential-backoff retry. See the dedicated AI Scalar Functions page for service configuration, failure semantics, and cost guidance.

FunctionSignatureDescription
AI_COMPLETE(prompt) / AI_COMPLETE(prompt, max_tokens)(VARCHAR [, BIGINT]) -> VARCHARFree-form completion. The 2-arg form sets the requested response-token limit per call, subject to service policy and your organization's AI Governance policy.
AI_GENERATE(prompt)(VARCHAR) -> VARCHARVendor-neutral alias of AI_COMPLETE(prompt).
AI_SUMMARIZE(text)(VARCHAR) -> VARCHARTerse 1–3 sentence summary.
AI_CLASSIFY_TEXT(text, labels)(VARCHAR, ARRAY<VARCHAR>) -> VARCHARPick exactly one label from the supplied array.
AI_FILTER(prompt, text)(VARCHAR, VARCHAR) -> BOOLEANBoolean LLM predicate — TRUE when text satisfies the natural-language prompt (arg 0 must be a string literal). Useful inline in WHERE / HAVING.
AI_TRANSLATE(text, target_language)(VARCHAR, VARCHAR) -> VARCHARTranslate text into the named target language.
AI_EXTRACT_ANSWER(text, question)(VARCHAR, VARCHAR) -> VARCHARExtract the answer to question from text. Returns SQL NULL when the source doesn't contain the answer.
AI_SENTIMENT(text)(VARCHAR) -> VARCHARSentiment label: positive, negative, or neutral.
AI_FIX_GRAMMAR(text)(VARCHAR) -> VARCHARGrammar/spelling/punctuation correction, meaning preserved.
AI_MASK(text, pii_types)(VARCHAR, ARRAY<VARCHAR>) -> VARCHARRedact the listed PII entity types (empty array = all common PII). AI_REDACT is an alias.
AI_EXTRACT(text, json_schema)(VARCHAR, VARCHAR) -> VARCHARStructured extraction into a JSON object matching json_schema.
AI_SQL(request)(VARCHAR) -> VARCHARNatural-language → a single SQL statement (schema-agnostic; use ASK for grounded NL2SQL).
AI_VALIDATE(text, criteria)(VARCHAR, VARCHAR) -> VARCHARLLM data-quality check; returns {"valid": <bool>, "reason": <text>} JSON.
AI_RERANK(query, candidates [, k])(VARCHAR, ARRAY<VARCHAR> [, BIGINT]) -> ARRAY<VARCHAR>Listwise LLM rerank: reorder candidates by relevance to query and return the top k (all of them if k omitted). Returns SQL NULL for a NULL query or empty candidate array.
AI_DEDUPE(items, threshold)(ARRAY<VARCHAR>, DOUBLE) -> ARRAY<VARCHAR>Fuzzy entity resolution: LLM groups near-duplicate entries and returns one canonical value per group. threshold (0–1) tunes merge strictness. Call over ARRAY_AGG(col).
AI_FORECAST(series, horizon [, season])(ARRAY<DOUBLE>, BIGINT [, BIGINT]) -> ARRAY<DOUBLE>Time-series forecast via native ARIMA/SARIMA — horizon future points. No LLM/API key required (always-on). Order the series with ARRAY_AGG(v ORDER BY ts).
AI_RAG(question, table, embed_col, k)(VARCHAR, table, column, INT) -> VARCHAROne-shot retrieval-augmented generation: embed question, vector-search the top k rows of table by embed_col, answer from them via AI_COMPLETE. embed_col may be a vector column (searched directly) or a text column (embedded on the fly). See AI Scalar Functions.
AI_SIMILARITY(a, b)(VARCHAR, VARCHAR) -> DOUBLECosine similarity of the two inputs' embeddings (1.0 = identical). Composes EMBED + COSINE_DISTANCE; needs the embedding provider configured.
AI_EMBED(text [, model])(VARCHAR [, VARCHAR]) -> FixedSizeList<Float32, N>Alias of EMBED.

Every LLM-backed scalar also has a TRY_-prefixed twin (TRY_AI_COMPLETE, TRY_AI_SENTIMENT, TRY_AI_RERANK, TRY_AI_DEDUPE, …) that returns SQL NULL for any cell whose LLM call fails instead of failing the query. (AI_FORECAST is the exception — it runs a classical ARIMA/SARIMA model, not an LLM, so it needs no API key and has no TRY_ twin.) Full configuration, error-handling, and cost guidance: AI Scalar Functions.

-- Classify support tickets in flight
SELECT ticket_id,
AI_CLASSIFY_TEXT(message,
ARRAY['billing', 'technical', 'feature_request', 'spam']) AS category,
AI_SUMMARIZE(message) AS tldr,
AI_SENTIMENT(message) AS mood
FROM support_tickets
WHERE created_at > now() - INTERVAL '1 hour';

Variant Functions (v3)​

These functions operate on the VARIANT type, available on Iceberg format version 3 tables.

FunctionDescription
PARSE_JSON(string)Parse a JSON string into VARIANT
TO_VARIANT(expr)Convert a scalar value to VARIANT
VARIANT_GET(v, path, type)Extract a typed value at a dot-path (e.g., VARIANT_GET(v, 'user.name', 'VARCHAR'))
VARIANT_EXTRACT_STRING(v, path)Extract value as a plain string
VARIANT_TYPE(v)Return the runtime type name ('object', 'string', 'int64', etc.)
VARIANT_AS_BOOL(v)Cast VARIANT to BOOLEAN
VARIANT_AS_INT(v)Cast VARIANT to BIGINT
VARIANT_AS_DOUBLE(v)Cast VARIANT to DOUBLE
VARIANT_AS_STRING(v)Cast VARIANT to VARCHAR
IS_VARIANT_NULL(v)True if the VARIANT value is null
SELECT
VARIANT_GET(payload, 'user', 'VARCHAR') AS user_name,
VARIANT_GET(payload, 'metrics.latency_ms', 'DOUBLE') AS latency
FROM events
WHERE VARIANT_TYPE(payload) = 'object';

Spatial Functions (v3)​

Spatial functions for GEOMETRY and GEOGRAPHY types, available on Iceberg format version 3 tables. Follows PostGIS naming conventions.

FunctionDescription
ST_POINT(x, y)Construct a point geometry. x / y are auto-coerced to Float64 from any signed-integer or Float32 input — ST_POINT(0, 0) and ST_POINT(CAST(0 AS REAL), 0) work without an explicit cast (matches PG / DuckDB).
ST_DISTANCE(g1, g2)Euclidean distance (GEOMETRY) or haversine distance (GEOGRAPHY)
ST_WITHIN(g1, g2)True if g1 is entirely within g2
ST_CONTAINS(g1, g2)True if g1 contains g2
ST_INTERSECTS(g1, g2)True if geometries share any point
ST_AREA(g)Area of a polygon
ST_LENGTH(g)Length of a linestring
ST_ASTEXT(g)Convert to Well-Known Text (WKT)
ST_GEOMFROMTEXT(wkt)Parse WKT string to GEOMETRY
ST_GEOGFROMTEXT(wkt)Parse WKT string to GEOGRAPHY
ST_X(g) / ST_Y(g)Extract X/Y coordinates from a point
ST_SRID(g)Get the spatial reference identifier
ST_TRANSFORM(g, target_srid)Reproject to a different coordinate reference system
ST_BUFFER(g, distance)Compute a buffer polygon around the geometry
ST_DWITHIN(g1, g2, distance)True if distance between geometries ≤ threshold
ST_ENVELOPE(g)Compute the bounding box as a polygon
ST_CENTROID(g)Centroid (POINT) of any geometry
ST_CONVEX_HULL(g)Convex hull polygon over a multi-point / multi-geometry input
ST_BOUNDARY(g)Boundary geometry — endpoints for a LINESTRING, ring for a POLYGON, etc.
ST_GEOMETRY_TYPE(g)Type name as VARCHAR, upper-case: POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION
ST_NUMPOINTS(g)Total number of coordinate vertices, including across the parts of a Multi* or GeometryCollection

Supported CRS: WGS84 (EPSG:4326), Web Mercator (EPSG:3857), UTM zones (EPSG:326xx/327xx).

-- Find stores within 5km of a location
SELECT name, ST_DISTANCE(location, ST_POINT(-73.98, 40.75)) AS dist_m
FROM stores
WHERE ST_DWITHIN(location, ST_POINT(-73.98, 40.75), 5000)
ORDER BY dist_m;

H3 spatial indexing​

Uber H3 hexagonal cell index. Cell IDs are returned as BIGINT so they can be partition keys, join keys, or grouping keys at full engine speed without serialization round-trips.

FunctionDescription
h3_latlng_to_cell(lat, lng, resolution)Encode a lat/lng pair at a given resolution (0–15) into an H3 cell ID
h3_cell_to_lat(cell) / h3_cell_to_lng(cell)Decode the center of a cell back to lat / lng
h3_is_valid(cell)True if the integer is a well-formed H3 cell ID
h3_get_resolution(cell)Cell's resolution level
h3_cell_to_parent(cell, resolution)Parent cell at the requested coarser resolution
h3_cell_to_children(cell, resolution)Array of child cells at the requested finer resolution
h3_grid_disk(cell, k)Array of cells within k rings of cell (inclusive)
h3_grid_distance(a, b)Hex grid distance between two cells (NULL if not in the same resolution / region)
-- "Pings within 1 km of any of these venues" via H3 prefix join
SELECT v.name, COUNT(*) AS hits
FROM venues v
JOIN pings p ON p.h3_r9 IN (
SELECT cell FROM UNNEST(H3_GRID_DISK(v.h3_r9, 1)) AS t(cell)
)
GROUP BY v.name;

S2 spatial indexing​

Google S2 spherical cell index. Same shape as the H3 helpers but for S2 levels (0–30).

FunctionDescription
s2_latlng_to_cell(lat, lng, level)Encode a lat/lng at a given S2 level into a cell ID
s2_cell_to_lat(cell) / s2_cell_to_lng(cell)Decode the center back to lat / lng
s2_is_valid(cell)True if the integer is a well-formed S2 cell ID
s2_cell_level(cell)S2 level of the cell
s2_cell_to_parent(cell, level)Parent cell at the requested coarser level

Session Functions​

Introspect the current session's identity and authorization context. Useful in row-level security policies and audit queries.

FunctionDescription
current_user()Authenticated username
current_roles()User's roles (array)
has_role(role)True if user has the specified role
session_id()Current session ID

Aggregate Functions​

Aggregate functions compute a single result from a set of input rows. They can be used in SELECT with GROUP BY, in HAVING clauses, and as window functions with OVER. approx_count_distinct uses HyperLogLog for fast cardinality estimation with ~2% relative error.

FunctionDescription
count(*) / count(expr)Row count
count(DISTINCT expr)Distinct count (COUNT(DISTINCT *) is rejected — see below)
sum(expr)Sum
avg(expr)Average
min(expr) / max(expr)Minimum / maximum
any_value(expr) / arbitrary(expr)Arbitrary non-NULL value (ARBITRARY is the Trino / Presto spelling)
first(expr) / last(expr)First / last value
stddev(expr) / stddev_samp(expr) / stddev_pop(expr)Sample / population standard deviation
variance(expr) / var_samp(expr) / var_pop(expr)Sample / population variance
string_agg(expr, delimiter) / listagg(expr, delimiter) / list_agg(expr, delimiter) / array_to_string_agg(expr, delimiter)Concatenate strings with separator (LISTAGG / LIST_AGG are DuckDB / Trino spellings)
array_agg(expr) / arrayagg(expr) / collect_list(expr) / collect_set(expr)Collect values into array (works under GROUP BY); COLLECT_SET deduplicates. Narrow integer / float input types are widened in the output element type — Int8 / Int16 / Int32 / UInt8 / UInt16 / UInt32 all collect into List(Int64); Float32 collects into List(Float64). The binder, analyzer, and decorrelator now agree on this rule, so a correlated ARRAY_AGG(int_col) reports the same List(Int64) schema as the uncorrelated form.
json_array_agg(expr) / json_agg(expr)Collect values into a JSON array string
json_object_agg(key, value)Collect key/value pairs into a JSON object string
percentile_cont(p) WITHIN GROUP (ORDER BY expr)Continuous percentile
percentile_disc(p) WITHIN GROUP (ORDER BY expr)Discrete percentile
median(expr)Median — continuous interpolation (alias for percentile_cont(0.5))
mode(expr)Most frequent value (returns Float64 for numeric / temporal / boolean inputs; preserves Utf8 / LargeUtf8 for string inputs)
grouping(col [, col, ...]) / grouping_id(col [, col, ...])Bitmask identifying which GROUP BY columns are aggregated in the current row (ROLLUP / CUBE / GROUPING SETS)
approx_count_distinct(expr)Approximate distinct count (HyperLogLog)
bool_and(expr) / bool_or(expr)Boolean aggregate AND / OR
bit_and(expr) / bit_or(expr) / bit_xor(expr)Bitwise aggregates
corr(y, x)Pearson correlation coefficient
covar_pop(y, x) / covar_samp(y, x)Population / sample covariance
regr_slope(y, x) / regr_intercept(y, x)Linear regression
regr_r2(y, x)R-squared
regr_count(y, x)Count of non-NULL pairs
regr_avgx(y, x) / regr_avgy(y, x)Mean of x / y over rows where both x and y are non-NULL
regr_sxx(y, x) / regr_syy(y, x) / regr_sxy(y, x)Sum of squared deviations of x / y / cross-product, over pair-non-NULL rows
arg_min(val, key) / arg_max(val, key) / min_by(val, key) / max_by(val, key)Value at min/max key. MIN_BY / MAX_BY are the Spark spellings. Known limitation: the global (ungrouped) form can return NULL when the query runs in parallel. Use a ROW_NUMBER() OVER (ORDER BY key) = 1 filter instead.
count_if(condition)Count where condition is true
kurtosis(expr) / skewness(expr)Distribution shape
entropy(expr)Shannon entropy in bits
histogram(expr)Frequency histogram (returns map)
kahan_sum(expr)Numerically stable sum (Kahan compensated)
product(expr)Multiplicative aggregate (always returns Float64, regardless of input type)

Approximate aggregates​

For high-cardinality columns where an exact result is expensive but a small bounded error is acceptable.

FunctionDescription
approx_top_k(col [, k])Top-k most frequent values (Space-Saving sketch). Returns a JSON string of {value, estimated_count} entries. Default k = 10. Works under GROUP BY and combines correctly across partitions (the final phase truncates back to k after merging partials).
approx_percentile(col, p)Approximate percentile via a t-digest sketch. p ∈ [0, 1]. Faster than percentile_cont for very large inputs and bounded relative error (≈ 1% by default).
hll_sketch(col)Build a HyperLogLog sketch for distinct-cardinality estimation. Returns a base64-encoded self-describing sketch (GHLL magic header + 16384-bucket payload). Persist as VARCHAR to compose sketches across shards / partitions / time windows.
hll_merge(sketch_col)Aggregate over an existing column of hll_sketch outputs; pointwise-max combines the buckets. Returns a sketch of the same shape. Use approx_count_distinct(col) for a one-shot cardinality, or call hll_merge and decode externally.
-- Top categories per partition, then a roll-up across partitions
SELECT region, APPROX_TOP_K(product, 5) AS top5
FROM events
GROUP BY region;

-- Pre-aggregated HLL sketches per day, merged at query time
WITH per_day AS (
SELECT day, HLL_SKETCH(user_id) AS sk FROM events GROUP BY day
)
SELECT HLL_MERGE(sk) AS sketch_for_window
FROM per_day
WHERE day BETWEEN DATE '2026-01-01' AND DATE '2026-01-31';

Statistical tests​

Two-sample hypothesis tests over paired numeric columns. Each test ships with a _P sibling that returns the p-value directly; the bare form returns the test statistic.

FunctionDescription
t_test(a, b) / t_test_p(a, b)Welch's t-test on two numeric columns (unequal variances). Returns the t-statistic / two-sided p-value.
chi_square_test(a, b) / chi_square_test_p(a, b)Pearson chi-square test of independence over paired observations. Returns the test statistic / p-value.
mann_whitney(a, b) / mann_whitney_p(a, b)Mann-Whitney U test (rank-based, non-parametric). Returns the U statistic / two-sided p-value.
-- A/B test: did the new ranker move click-through?
SELECT
T_TEST(arm_a_ctr, arm_b_ctr) AS t,
T_TEST_P(arm_a_ctr, arm_b_ctr) AS p_value
FROM ab_results;
-- Comma-separated list of regions per customer
SELECT customer_id, string_agg(region, ', ' ORDER BY region) AS regions
FROM orders GROUP BY customer_id;

-- 95th percentile of response times
SELECT percentile_cont(0.95) WITHIN GROUP (ORDER BY latency_ms) AS p95
FROM requests;

-- Which product had the highest total revenue?
SELECT arg_max(product_name, total_revenue) AS top_product
FROM (SELECT product_name, SUM(amount) AS total_revenue FROM sales GROUP BY 1);

-- Count orders over $100 without a subquery
SELECT count_if(amount > 100) AS big_orders, COUNT(*) AS total FROM orders;

-- Quick frequency distribution
SELECT histogram(status) FROM orders;
-- {'pending': 150, 'shipped': 820, 'cancelled': 30}

-- Linear regression: price sensitivity
SELECT regr_slope(quantity, price) AS price_sensitivity,
regr_r2(quantity, price) AS r_squared
FROM products;

-- ARRAY_AGG with GROUP BY collects per-group lists
SELECT region, ARRAY_AGG(customer_id ORDER BY customer_id) AS customers
FROM orders
GROUP BY region;

AISQL aggregates (LLM-backed)​

LLM-backed aggregates that fold a group of text rows into one result via a binder rewrite (STRING_AGG over the group, then a scalar AI call). Use them with GROUP BY, or without — an ungrouped query folds the whole result set into a single group. Each call hits the configured AI provider; see AI Scalar Functions for provider availability and cost guidance and the AISQL cookbook for worked examples.

FunctionDescription
AI_AGG(prompt, text)Apply the natural-language prompt (arg 0, a string literal) across all text values in the group; returns the LLM's response.
AI_SUMMARIZE_AGG(text)One summary of all text values in the group.
AI_CLASSIFY_AGG(text, labels)Pick one label from labels (an ARRAY<VARCHAR>) for the group's combined text.
AI_FILTER_AGG(prompt, text)Boolean — TRUE when the group's combined text satisfies the natural-language prompt (arg 0 a string literal).

Each of these has a TRY_-prefixed variant (TRY_AI_AGG, TRY_AI_SUMMARIZE_AGG, TRY_AI_CLASSIFY_AGG, TRY_AI_FILTER_AGG) that yields SQL NULL for a group whose LLM call fails instead of failing the whole query.

SELECT region, AI_SUMMARIZE_AGG(review) AS summary
FROM reviews
GROUP BY region;

Aggregate semantics notes​

MEDIAN — continuous interpolation​

MEDIAN(x) lowers to PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x), matching Postgres / DuckDB / Oracle. Even-cardinality inputs return the linear interpolation of the two middle values, e.g. MEDIAN(1, 2, 3, 4) = 2.5. Use PERCENTILE_DISC(0.5) explicitly if you want the discrete lower-middle (= 2).

PERCENTILE_CONT(p) / PERCENTILE_DISC(p) — p literal type​

The percentile argument p may be written as any of: PERCENTILE_CONT(0.5) (unquoted decimal — Decimal128), PERCENTILE_CONT(1) / PERCENTILE_CONT(0) (integer endpoint), or PERCENTILE_CONT(CAST(0.95 AS DOUBLE)) (explicit float). All three are recognised by the bind-time domain check and by the planner's param_value extraction, so the value you write is the percentile the aggregate computes. Earlier the unquoted-decimal form (PERCENTILE_CONT(0.9)) fell through to a 0.5 default at the planner because the Decimal128 shape wasn't recognised — the aggregate then silently returned the median.

STDDEV_SAMP / VAR_SAMP with n=1​

Sample standard deviation and sample variance return NULL when the group has exactly one non-NULL row (the n − 1 divisor is undefined). Population variants (STDDEV_POP / VAR_POP) return 0 for a single-row group as before. Matches Postgres.

Regression aggregates — pair-NULL handling​

All two-argument regression aggregates (CORR, COVAR_POP, COVAR_SAMP, REGR_SLOPE, REGR_INTERCEPT, REGR_R2, REGR_COUNT, REGR_AVGX, REGR_AVGY, REGR_SXX, REGR_SYY, REGR_SXY) skip rows where either x or y is NULL — every component sums over the same pair-non-NULL row set. This matches the SQL standard and Postgres; e.g. CORR over (1,1), (2,NULL), (3,3) returns 1.0 (the pair (2, NULL) is dropped from every component, not just from REGR_COUNT). Earlier builds let individual components see different row masks, which silently produced wrong correlations and the NULL defence-in- depth fall-back on legitimate inputs.

SUM(DISTINCT expr) / AVG(DISTINCT expr) over an expression​

SUM(DISTINCT expr) and AVG(DISTINCT expr) now work for any expression, not just bare columns. Earlier the rewriter that pre-deduplicates (group_by, dedup_expr) left the outer aggregate referencing the original expression while the inner Aggregate's output schema named the synthetic column __group_0, so SUM(DISTINCT qty + 1) either errored (Column 'qty' not found) or silently returned NULL:

-- Now returns the distinct-sum of (qty + 1) per group
SELECT region, SUM(DISTINCT qty + 1) AS s, AVG(DISTINCT qty + 1) AS a
FROM orders
GROUP BY region;

COUNT(DISTINCT expr) and MIN(DISTINCT expr) / MAX(DISTINCT expr) were never affected — COUNT uses a different codepath, and MIN / MAX are dedup-insensitive so the rewriter strips DISTINCT in place.

Multiple DISTINCT aggregates with different arguments​

A single GROUP BY scope that mixes SUM/AVG/STDDEV/VAR DISTINCT over different argument expressions, or mixes a duplicate-sensitive DISTINCT aggregate with a duplicate-sensitive non-DISTINCT aggregate, is rejected at bind time:

-- Error: the pre-dedup rewrite needed for SUM(DISTINCT v) would
-- change the row count seen by the plain SUM(v).
SELECT SUM(DISTINCT v), SUM(v) FROM t;

Earlier this silently returned the non-distinct sum for both slots (the runtime hash-aggregate only honours DISTINCT for COUNT). Rewrite as separate scalar subqueries or a UNION ALL of single-bucket aggregations:

SELECT (SELECT SUM(v) FROM (SELECT DISTINCT v FROM t)) AS sum_distinct,
(SELECT SUM(v) FROM t) AS sum_all;

COUNT(DISTINCT a), COUNT(DISTINCT b) is unaffected (the COUNT codepath dedups independently), and a single DISTINCT bucket combined with dedup-insensitive MIN/MAX over the same argument still works.

GROUPING() / GROUPING_ID() argument must be a grouping key​

In a plain GROUP BY (no ROLLUP / CUBE / GROUPING SETS), GROUPING(col) is only meaningful for a column that appears in the GROUP BY clause (it always returns 0 there). Calling it on a non-grouping column now errors at bind time instead of silently returning 0 (which falsely implied the column was a grouping key):

-- Error: 'b' is not a grouping expression of this query level
SELECT a, GROUPING(b) FROM t GROUP BY a;

-- OK
SELECT a, GROUPING(a) FROM t GROUP BY a; -- always 0
SELECT a, GROUPING(a) FROM t GROUP BY ROLLUP(a); -- 0 or 1 per subtotal

COUNT(DISTINCT *) is rejected​

COUNT(DISTINCT *) raises a bind-time error rather than silently counting all rows. The semantics aren't well-defined cross-engine (Postgres rejects it). Rewrite as COUNT(DISTINCT (col1, col2, ...)) (composite distinct) or COUNT(*) if you actually wanted a row count.

ROLLUP / CUBE / GROUPING SETS over empty input​

When the input table is empty, queries with GROUP BY ROLLUP(...) / CUBE(...) / GROUPING SETS(... ()) still emit one grand-total row per () subset — matching Postgres / DuckDB. Earlier the empty-input path skipped the Partial-side synthesis, so SELECT SUM(x) FROM empty GROUP BY ROLLUP(a) returned zero rows instead of one grand-total row with NULL group keys.

-- Returns one row: (NULL, 0) — the grand total over zero rows
SELECT a, COUNT(*) FROM (SELECT 1 AS a WHERE FALSE) t
GROUP BY ROLLUP(a);

Correlated ANY_VALUE(int) / MODE(int) preserve input type​

The decorrelator's per-aggregate schema-build rule for ANY_VALUE / ARBITRARY / MODE preserves the input numeric type. A correlated (SELECT ANY_VALUE(int_col) FROM t WHERE t.k = a.k) returns Int32 instead of Float64 (matching the uncorrelated form and the binder's schema for the same aggregate).

GROUPING / GROUPING_ID multi-arg form​

GROUPING(a, b, …) and the alias GROUPING_ID(a, b, …) accept multiple arguments and return a bitmask where bit n − i − 1 is set if column i is rolled up in the current output row. Useful for distinguishing subtotal rows produced by ROLLUP, CUBE, or GROUPING SETS:

SELECT region, city,
GROUPING(region, city) AS gid,
SUM(amount) AS total
FROM orders
GROUP BY ROLLUP(region, city)
ORDER BY gid;
-- gid=0 → (region, city) leaf rows
-- gid=1 → (region) subtotals (city is rolled up)
-- gid=3 → grand total (both rolled up)

FILTER (WHERE …) clause​

Aggregate functions support a FILTER (WHERE predicate) clause that limits the rows fed into the aggregate without affecting other aggregates in the same SELECT:

SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'shipped') AS shipped,
SUM(amount) FILTER (WHERE region = 'US') AS us_revenue
FROM orders;

FILTER is also valid on aggregate window functions (e.g. SUM(...) FILTER (...) OVER (...)) but is rejected at bind time on non-aggregate window functions (RANK, ROW_NUMBER, NTILE, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTH_VALUE, PERCENT_RANK, CUME_DIST). Postgres has the same restriction; the clause was previously silently dropped, hiding user intent.

Window Functions​

Window functions compute a value for each row over a sliding frame of related rows, without collapsing groups. All aggregate functions can also be used as window functions with an OVER clause. See SELECT: Window Functions for frame syntax (ROWS, RANGE, GROUPS).

FunctionDescription
row_number()Sequential row number
rank()Rank with gaps
dense_rank()Rank without gaps
percent_rank()Relative rank (0.0 to 1.0)
cume_dist()Cumulative distribution
lag(expr [, offset [, default]])Value from offset rows before; default must be type-compatible with expr
lead(expr [, offset [, default]])Value from offset rows after; default must be type-compatible with expr
first_value(expr)First value in the frame (frame-aware — see notes below)
last_value(expr)Last value in the frame (frame-aware — see notes below)
nth_value(expr, n)Nth value in the frame (n must be ≥ 1)
ntile(k)Divide into k buckets; remainder rows go to the early buckets

Window-function semantics notes​

Frame-aware FIRST_VALUE / LAST_VALUE / NTH_VALUE​

These three functions honour the window frame, not the partition, matching Postgres and the SQL standard. With the default frame (ROWS UNBOUNDED PRECEDING TO CURRENT ROW):

  • FIRST_VALUE(x) OVER (ORDER BY ts) returns the first row's value in the partition (frame starts at UNBOUNDED PRECEDING).
  • LAST_VALUE(x) OVER (ORDER BY ts) returns the current row's value (the frame ends at CURRENT ROW).

To get partition-end semantics for LAST_VALUE, set the frame explicitly:

LAST_VALUE(x) OVER (
ORDER BY ts
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)

Earlier builds applied these functions to the full partition regardless of the frame clause, which silently disagreed with Postgres / DuckDB.

NTILE(k) remainder distribution​

NTILE(k) distributes the remainder N % k extras to the early buckets, matching Postgres and the SQL standard. For 10 rows split into 3 buckets, sizes are [4, 3, 3] (not [4, 4, 2]).

NTH_VALUE(x, n) requires n ≥ 1​

NTH_VALUE(expr, 0) raises a bind-time error (Postgres rejects n ≤ 0). Pass 1 for the first frame row, or use FIRST_VALUE(expr).

LAG / LEAD default-value type check​

The optional default argument to LAG / LEAD must be type- compatible with the value expression — numeric defaults for numeric columns, string defaults for string columns, etc. NULL defaults are always accepted and intra-kind widening (e.g., INT default for a BIGINT column) is allowed. Cross-kind defaults (string default for a numeric column) raise a bind error mirroring Postgres' "function lag(integer, integer, unknown) does not exist".

LAG / LEAD default must be a literal​

The default argument must be a literal (string, number, boolean, NULL, or a typed literal like DATE '2024-01-01'). A per-row default expression — anything that references a column — is rejected at bind time with:

LAG's default value must be a literal — per-row default
expressions are not supported

Earlier builds silently produced NULL on the boundary row instead of evaluating the per-row default, which masked user intent.

-- OK: literal default
LAG(v, 1, 0) OVER (ORDER BY ts)
LAG(v, 1, NULL) OVER (ORDER BY ts)

-- Error: per-row default expression
LAG(v, 1, v * 10) OVER (ORDER BY ts)

If you need a per-row fallback, materialise the expression via COALESCE after the LAG:

SELECT COALESCE(LAG(v) OVER (ORDER BY ts), v * 10) FROM t;

Sequence Functions​

FunctionDescription
nextval('sequence_name')Advance the sequence and return the next value
currval('sequence_name')Return the last value generated in this session (no advance)

See CREATE SEQUENCE for creating and managing sequences.