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: bind values to INPUT() features
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
; Key rules:
WITHfeatures are not in the output.SELECTfeatures are in the output.- Features can reference each other in any order — dependency resolution is automatic.
FROM fm.namespaceimports persisted features so you can reference them by short name.- Trailing commas are allowed everywhere.
Assignment styles
All three are equivalent. The := style is preferred:
NUMBER1 := 1 -- FeatureQL style (preferred)
1 AS NUMBER1 -- SQL style
NUMBER1 IS 1 -- Alternative Type system
No automatic coercion — cast explicitly with ::TYPE or CAST(x AS TYPE).
Scalar types
| Category | Types | Aliases | Literal examples |
|---|---|---|---|
| Integer | BIGINT, MEDIUMINT, SMALLINT, TINYINT | INT64, INT32, INT16, INT8 | 42, -7 |
| Decimal | DECIMAL | — | 1.25 (auto-detects precision: DECIMAL(3,2)) |
| Float | FLOAT, DOUBLE | FLOAT32, FLOAT64 | 1.25e0 (scientific notation) |
| String | VARCHAR | — | 'text' (single quotes only) |
| Boolean | BOOLEAN | — | TRUE, FALSE |
| Date | DATE | — | DATE '2024-10-15' |
| Timestamp | TIMESTAMP | — | TIMESTAMP '2024-10-15 10:04:00' |
| Interval | INTERVAL | — | INTERVAL '3' DAY |
| JSON | JSON | — | JSON '"value"' |
Complex types
| Type | Alias | Literal 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 Special values
| Value | Meaning |
|---|---|
NULL | Untyped null |
NULL(DOUBLE) | Typed null |
EMPTY | Explicitly 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 Critical type pitfalls
DECIMAL coercion is inconsistent. Scalar arithmetic between different DECIMAL precisions works: 1.1 + 1.25 produces DECIMAL(4,2). But DECIMAL coercion does not apply inside array or ROW constructors:
-- OK: scalar arithmetic coerces DECIMAL(2,1) + DECIMAL(3,2) → DECIMAL(4,2)
total := 1.1 + 1.25 -- Works, result is 2.35
-- FAILS: array/ROW constructors require identical types
arr := ARRAY[ROW(1.1 AS dec), ROW(1.25 AS dec)]
-- Error: ROW(dec DECIMAL(2,1)) ≠ ROW(dec DECIMAL(3,2)) Rule of thumb: use DOUBLE (scientific notation) for decimal values in arrays, ROWs, and mixed contexts. DECIMAL is safe for simple scalar arithmetic but will bite you in constructors.
-- 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)
-- WRONG: different DECIMAL precisions in array constructor
arr := ARRAY[ROW(1.1 AS val), ROW(1.25 AS val)]
-- RIGHT: use DOUBLE in constructors
arr := ARRAY[ROW(1.1e0 AS val), ROW(1.25e0 AS val)]
-- WRONG: integer + decimal is a type error
total := 1 + 1.0
-- RIGHT: cast to same type
total := 1::DOUBLE + 1.0e0 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 for decimal numbers, especially in arrays and ROWs.
Operators (by precedence, highest first)
| Precedence | Operators | Description |
|---|---|---|
| 1 | () | Parentheses |
| 2 | := | Assignment |
| 3 | [] | Array/row field extraction |
| 4 | :: | Type casting |
| 5 | . * / // % ^ | Chaining, multiply, divide, integer div, modulo, power |
| 6 | + - | Addition, subtraction |
| 7 | = <> != > >= < <= BETWEEN LIKE IN | Comparison |
| 8 | AND OR NOT | Logical |
Special operators
| Operator | Purpose | Example |
|---|---|---|
=.N | Float comparison with N digits precision | 0.99999e0 =.4 1.00001e0 → TRUE |
|| | String concatenation | 'Hello' || ' World' |
// | Integer division | 7 // 3 → 2 |
[] | Extract field from ROW or element from ARRAY | row[field_name], array[1] |
IS NULL / IS NOT NULL | Null checks | value 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) 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'] Conditional expressions
-- Ternary
IF(condition, then_value, else_value)
-- Multi-branch
CASE WHEN(
ARRAY[condition1, condition2], -- Boolean conditions
ARRAY[value1, value2], -- Corresponding results
default_value -- Fallback
)
-- Value matching
CASE WHEN VALUE(
expression, -- Value to match
ARRAY[match1, match2], -- Match values
ARRAY[result1, result2], -- Corresponding results
default_value -- Fallback
)
-- First non-null
COALESCE(value1, value2, value3) 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 := @literal(POWER(2, 10)),
-- Non-deterministic functions MUST be wrapped in @literal
TODAY := @literal(CURRENT_TIMESTAMP()), -- Evaluated once, not per row
RAND := @literal(RANDOM()), -- Evaluated once, not per row
-- Inject computed string into query text
DYNAMIC := @echo('SELECT * FROM ' || table_name),
-- Deterministic per-row randomness (NOT @literal — this is per-row by design)
BUCKET := HASH01(USER_ID, 'experiment_name'), -- Returns 0.0 to 1.0 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 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