LLM reference — FeatureQL syntax

Complete syntax reference for writing FeatureQL queries. Load this page when you need to write or debug query structure, types, casting, operators, or expression syntax.

Query anatomy

CONST                          -- Optional: compile-time constants
    MY_CONST := 42,
WITH                           -- Local features (only evaluated if referenced in SELECT)
    FEATURE1 := INPUT(BIGINT),
    FEATURE2 := FEATURE1 * 2,
SELECT                         -- Features to return in output
    FEATURE1,
    FEATURE2,
FROM fm.namespace              -- Optional: pull persisted features from registry
FOR                            -- Required when SELECT depends on INPUT(); omit if no INPUT() in that graph
    FEATURE1 := BIND_VALUES(ARRAY[1, 2, 3]),
WHERE FEATURE2 > 2             -- Optional: filter result rows
ORDER BY FEATURE2 DESC         -- Optional: sort
LIMIT 10 OFFSET 5              -- Optional: pagination
;
sql

Key rules:

  • WITH features are not in the output. SELECT features are in the output.
  • Features can reference each other in any order — dependency resolution is automatic.
  • FROM fm.namespace imports persisted features so you can reference them by short name.
  • FOR: required iff any INPUT() appears in the dependency graph of your SELECT (directly or through other features). Literal-only queries with no INPUT() may omit FOR. Multiple independent BIND_* clauses need FOR CROSS when you intend a Cartesian product — see Binding inputs .
  • Trailing commas are allowed everywhere.

Assignment styles

Both are equivalent. The := style is preferred:

NUMBER1 := 1           -- FeatureQL style (preferred)
1 AS NUMBER1           -- SQL style
sql

Type system

No automatic coercion — cast explicitly with ::TYPE or CAST(x AS TYPE).

Scalar types

CategoryTypesAliasesLiteral examples
IntegerBIGINT, INT, SMALLINT, TINYINTINT64, INT32, INTEGER, INT16, INT842, -7
DecimalDECIMAL1.25 (auto-detects precision: DECIMAL(3,2))
FloatFLOAT, DOUBLEFLOAT32, FLOAT641.25e0 (scientific notation)
StringVARCHAR'text' (single quotes only)
BooleanBOOLEANTRUE, FALSE
DateDATEDATE '2024-10-15'
TimestampTIMESTAMPTIMESTAMP '2024-10-15 10:04:00'
IntervalINTERVALINTERVAL '3 DAY' (quantity and unit in one string; not INTERVAL '3' DAY)
JSONJSONJSON '"value"'

Complex types

TypeAliasLiteral example
ARRAY(T)LIST(T)ARRAY[1, 2, 3]
ROW(field1 T1, field2 T2)STRUCT(...)ROW(1, 'A' AS name)
ARRAY(ROW(...))ARRAY[ROW(1, 'A'), ROW(2, 'B')]::ARRAY(ROW(num BIGINT, letter VARCHAR))

Entity annotations

Entity annotations link types to semantic entities for relationship tracking:

BIGINT#CUSTOMERS           -- This value is a CUSTOMERS entity key
ARRAY(BIGINT#ORDERS)       -- Array of ORDER entity keys
sql

Special values

ValueMeaning
NULLUntyped null
NULL(DOUBLE)Typed null
EMPTYExplicitly empty (cannot be cast)

Casting

CAST('123' AS BIGINT)      -- Standard form
'123'::BIGINT              -- Shorthand (higher precedence than operators)
ID::BIGINT#ORDERS          -- Add entity annotation
ID::BIGINT                 -- Remove entity annotation
sql

Critical type pitfalls

DECIMAL widening works everywhere. Different DECIMAL precisions are unified (widened) automatically in arithmetic, arrays, ROWs, and constructors: ARRAY[1.25, 1.1] produces ARRAY(DECIMAL(3,2)).

Integer promotion to DECIMAL is limited. When a BIGINT literal appears next to a DECIMAL in arithmetic, comparisons, or BETWEEN, it is promoted to DECIMAL(N,0). This promotion does not apply inside ARRAY or ROW constructors — mixing BIGINT and DECIMAL elements is an error.

BETWEEN is strict: the value and both bounds must be the same types (e.g. all DOUBLE with e0 literals, or all BIGINT). Mixed BIGINT / DOUBLE fails.

-- OK: DECIMAL widening in arrays and ROWs (same family, different precision)
arr := ARRAY[1.25, 1.1]                            -- ARRAY(DECIMAL(3,2))
aor := ARRAY[ROW(1.1 AS val), ROW(1.25 AS val)]    -- ARRAY(ROW(val DECIMAL(3,2)))

-- OK: integer widening in arrays (all integers, different sizes)
arr := ARRAY[ROW(2::BIGINT), ROW(INT '2')]          -- ARRAY(ROW(field_1 BIGINT))

-- WRONG: BIGINT + DECIMAL in array (no promotion in constructors)
arr := ARRAY[1.25, 1]     -- Error: DECIMAL + BIGINT are different families

-- WRONG: 1.25 is DECIMAL(3,2), price is DOUBLE → type mismatch
revenue := price * 1.25

-- RIGHT: Use scientific notation for DOUBLE literals
revenue := price * 1.25e0

-- RIGHT: Or cast explicitly
revenue := price * CAST(1.25 AS DOUBLE)
sql

The e0 suffix means "times 10 to the power 0" — mathematically a no-op, but it tells FeatureQL the literal is DOUBLE, not DECIMAL. Use e0 when you need DOUBLE values.

Operators (by precedence, highest first)

PrecedenceOperatorsDescription
1:=Assignment
2()Parentheses
3[]Array/row extraction
4::Type casting
5.Function chaining
6~Bitwise NOT
7^ **Power
8* / // %Multiply, divide, integer div, modulo
9+ -Addition, subtraction
10<< >>Bitwise shift left, bitwise shift right
11&Bitwise AND
12|Bitwise OR
13||String concatenation
14= <> != > >= < <= BETWEEN NOT BETWEEN LIKE NOT LIKE IN NOT IN IS NULL IS NOT NULLComparison
15NOTLogical NOT
16ANDLogical AND
17ORLogical OR

Special operators

OperatorPurposeExample
=.NFloat comparison with N digits precision0.99999e0 =.4 1.00001e0TRUE
||String concatenation'Hello' || ' World'
//Type-safe division6.10 // 32.03
[]Extract field from ROW or element from ARRAYrow[field_name], array[1]
IS NULL / IS NOT NULLNull checksvalue IS NULL

Function chaining

Any function can be called as a method using . syntax. The expression before the dot becomes the first argument:

' HELLO '.TRIM().LOWER()              -- Equivalent to LOWER(TRIM(' HELLO '))
ARRAY[3,1,2].ARRAY_SORT().ARRAY_SUM() -- Chain array operations
(1 + 2).MULTIPLY(3)                   -- Equivalent to MULTIPLY((1 + 2), 3)
sql

Style preferences (readable, LLM-friendly code)

When you generate FeatureQL, default to forms that read like ordinary expressions: operators, SQL-style CASE, and left-to-right chains. Use function-call or “parallel array” forms when they are clearer or required by evaluation rules.

ValidPreferred (when equivalent)
ADD(a, b), SUBTRACT, MULTIPLY, DIVIDE, and other numeric variadic call formsa + b, a - b, a * b, a / b or a // b, a ^ b or a ** b
CONCAT(s1, s2) (two pieces of text)s1 || s2
LOWER(TRIM(x)), ARRAY_SUM(ARRAY_SORT(arr)), etc.x.TRIM().LOWER(), arr.ARRAY_SORT().ARRAY_SUM() when the pipeline reads naturally
CASE_WHEN(ARRAY[cond…], ARRAY[then…], else) with ordinary boolean branchesCASE WHEN … THEN … WHEN … THEN … ELSE … END
Deep nesting F(G(H(x)))Intermediate features in WITH, or method chaining on the inner value so readers scan in one direction
Unary / variadic builtins with no operator (e.g. ROUND, COALESCE, ARRAY_LENGTH)Keep the function — there is nothing to replace

Conditionals: Prefer IF(cond, then, else) for a single ternary. Prefer SQL CASE (searched or simple) for multi-branch logic. Use CASE_WHEN / CASE_WHEN_VALUE when the logic is naturally “parallel arrays of conditions and results” (or when tests/registry examples require that shape); see SHOW TESTS if unsure.

Chaining: Prefer .method() when it matches “take this value, then this step, then this step” and avoids a parenthesis stack. If a chain gets long, split across WITH assignments.

Array and ROW access with []

The [] operator is overloaded based on context:

-- ROW field access
row_feature[field_name]              -- Extract one field
row_feature[field1, field2]          -- Extract multiple fields (returns ROW)

-- ARRAY element access (1-indexed)
array_feature[1]                     -- First element
array_feature[2:4]                   -- Slice (elements 2, 3, 4)
array_feature[2,4]                   -- Elements at positions 2 and 4

-- ARRAY(ROW) field extraction (returns ARRAY of that field's values)
array_of_rows[field_name]            -- Extract field as array: [{id:1,name:'A'},{id:2,name:'B'}][name] → ['A','B']
sql

Conditional expressions

LLM style: Use the SQL-style forms below for most hand-written and generated queries. Reserve CASE_WHEN / CASE_WHEN_VALUE (documented just after this block) for parallel-array idioms or when the registry/tests expect that shape — see Style preferences .

-- Ternary
IF(condition, then_value, else_value)

-- Multi-branch
CASE
    WHEN condition1 THEN value1
    WHEN condition2 THEN value2
    ELSE default_value
END

-- Value matching
CASE expression
    WHEN match1 THEN result1
    WHEN match2 THEN result2
    ELSE default_value
END

-- First non-null
COALESCE(value1, value2, value3)
sql

Functional forms (not CASE WHEN( — that starts SQL case and expects THEN):

  • CASE_WHEN(ARRAY[cond1, cond2], ARRAY[then1, then2], else) — first true condition picks the parallel branch. The then array is often literal; dynamic shapes can hit evaluation-mode errors — see SHOW TESTS WHERE FUNCTION_NAME = 'CASE_WHEN'.
  • CASE_WHEN_VALUE(expr, ARRAY[match…], ARRAY[then…], else) — equality match / simple case; match and then arrays same length.

Metaprogramming

Use when you need compile-time evaluation or dynamic query construction:

CONST MY_VALUE := 42,                          -- Compile-time constant

-- Inject computed literal (preserves type, evaluated once at compile time)
THRESHOLD := @eval_as_literal(POWER(2, 10)),

-- Non-deterministic / compile-once scalars: wrap in @eval_as_literal(...) (not a separate @literal keyword)
TODAY := @eval_as_literal(CURRENT_TIMESTAMP()),         -- Evaluated once, not per row
RAND := @eval_as_literal(RANDOM()),                     -- Evaluated once, not per row

-- Inject computed string into query text (treat like SQL string splice — only trusted inputs)
DYNAMIC := @eval_as_text('SELECT * FROM ' || table_name),

-- Deterministic per-row randomness (NOT @eval_as_literal — per-row by design)
BUCKET := HASH01(USER_ID, 'experiment_name'),   -- Returns 0.0 to 1.0
sql

ARRAY_MERGE join spelling

The grammar accepts ARRAY_MERGE(left, right JOIN ON key_field) (keywords JOIN ON). Older prose sometimes says JOINED ON; use JOIN ON in new queries.

Persistence (DDL)

CREATE FEATURES IN fm.namespace AS SELECT ...;           -- Persist feature definitions
CREATE OR REPLACE FEATURES IN fm.namespace AS SELECT ...; -- Upsert
CREATE TEMPORARY FEATURES AS SELECT ...;                  -- Session-scoped
DROP FEATURES fm.namespace.feature_name;                  -- Remove
ALTER FEATURE fm.namespace.feature SET COMMENT = 'desc';  -- Add metadata
sql

Features are persisted as definitions (like SQL views), not data. FROM fm.namespace in a query imports them.

Namespace conventions

-- Features use fm.namespace.feature_name pattern
FROM fm.ecomm                         -- Import namespace, use short names
FROM fm.ns1, fm.ns2 AS _ns2           -- Multiple namespaces (alias must start with _)
fm.ecomm.CUSTOMER_NAME                -- Fully qualified reference
sql
Last update at: 2026/05/05 08:09:16