Functions — Advanced Patterns

Some FeatureQL operations exist in both operator and function forms. Most of the time the two are interchangeable, but a handful of names — FLATTEN, MERGE, COALESCE, and others — are ambiguous because the same name can refer to either form. This page explains how to resolve that ambiguity and covers a related edge case around array-based evaluation modes.

Operator vs. function form disambiguation

When you write FLATTEN(ARRAY['a', 'b']), FeatureQL cannot tell at parse time whether you mean the operator that flattens a nested array, or the function that flattens multiple features named a and b. Type inference happens after parsing, so the parser needs a syntactic hint.

The convention: append _FN to force the function form. The plain name always resolves to the operator form.

Operator form (variadic args)Function form (single array arg)
FLATTEN(expr1, expr2, ...)FLATTEN_FN(ARRAY[expr1, expr2, ...])
MERGE(expr1, expr2, ...)MERGE_FN(ARRAY[expr1, expr2, ...])
ARRAY_MERGE(expr1, expr2, ...)ARRAY_MERGE_FN(ARRAY[expr1, expr2, ...])
ZIP(expr1, expr2, ...)ZIP_FN(ARRAY[expr1, expr2, ...])
ARRAY_CONCAT(expr1, expr2, ...)ARRAY_CONCAT_FN(ARRAY[expr1, expr2, ...])
ALL(expr1, expr2, ...)ALL_FN(ARRAY[expr1, expr2, ...])
ANY(expr1, expr2, ...)ANY_FN(ARRAY[expr1, expr2, ...])
NONE(expr1, expr2, ...)NONE_FN(ARRAY[expr1, expr2, ...])
GREATEST(expr1, expr2, ...)GREATEST_FN(ARRAY[expr1, expr2, ...])
LEAST(expr1, expr2, ...)LEAST_FN(ARRAY[expr1, expr2, ...])
COALESCE(expr1, expr2, ...)COALESCE_FN(ARRAY[expr1, expr2, ...])
CONCAT(expr1, expr2, ...)CONCAT_FN(ARRAY[expr1, expr2, ...])
CONCAT_WS(sep, expr1, ...)CONCAT_WS_FN(sep, ARRAY[expr1, ...])

Use the operator form for straightforward expressions with a known number of arguments. Use the _FN form when you need to pass a dynamically constructed array or want to make intent explicit.

For the basics of operator/function equivalence, see Operators & Functions .

Array-based evaluation modes

Some functions accept arrays of conditions or values, enabling dynamic branching. CASE_WHEN() is the most common example — it takes an array of boolean conditions and an array of corresponding results, evaluating them row by row:

FeatureQL
WITH
    ID := INPUT(BIGINT)
SELECT
    ID := BIND_VALUES(ARRAY[1,2,3,4,5,6]),
    arr_if := ARRAY[ID IN (1,2), ID IN (3,4)],
    arr_then := ARRAY[ID, ID*2],
    CASE_WHEN(arr_if, arr_then, ID*3)
Result
ID BIGINTARR_IF ARRAYARR_THEN ARRAY?_3 VARCHAR
1[true, false][1, 2]1
2[true, false][2, 4]2
3[false, true][3, 6]6
4[false, true][4, 8]8
5[false, false][5, 10]15
6[false, false][6, 12]18

Each row gets its own condition/value arrays, so different rows can follow different evaluation paths. Row 1 matches the first condition (ID IN (1,2)), so it returns ID (which is 1). Row 5 matches neither condition, so it falls through to the default ID*3 (which is 15).

Static vs. dynamic arrays

There is a constraint: some function parameters require static arrays — arrays whose structure is known at compile time. An array literal like ARRAY[ID, ID*2] is static because the compiler can see it has exactly two elements. A function call like SEQUENCE(1, 2) produces an array at runtime, so the compiler cannot verify its shape.

FeatureQL
WITH
    ID := INPUT(BIGINT)
SELECT
    ID,
    arr_if := ARRAY[ID IN (1,2), ID IN (3,4)],
    arr_then := SEQUENCE(1, 2),  -- DOES NOT work
  	-- arr_then := ARRAY[1,2],  -- WORKS
    -- arr_then := @literal(SEQUENCE(1, 2)),  -- WORKS
    CASE_WHEN(arr_if, arr_then, ID*3)
FOR
	ID := BIND_VALUES(ARRAY[1,2,3,4,5,6])
;
Result
ERROR UE/EVALUATION-MODE-NOT-DYNAMIC-ARRAY CASE_WHEN() parameter 'then' must be shaped as a static array.

If you need a dynamically generated array in a position that requires a static one, wrap it with @literal() to force compile-time evaluation: @literal(SEQUENCE(1, 2)) works because the sequence is resolved before the query runs. See Metaprogramming for details on @literal().

Last update at: 2026/03/03 16:47:38
Last updated: 2026-03-03 16:48:19