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:
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)| ID BIGINT | ARR_IF ARRAY | ARR_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.
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])
;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().