Friendly syntax

FeatureQL adds several conveniences on top of standard SQL that make queries shorter and easier to maintain. None of these change what you can compute — they just make it more pleasant to write.

Automatic dependency resolution

In SQL, you must define things before you reference them. In FeatureQL, order doesn't matter — reference a feature before it's defined and the engine resolves the dependency graph for you:

FeatureQL
RETURN
    number3 := number1 + number2, -- Automatic Dependency Resolution: you can use features not yet defined
    number1 := 1,
    number2 := 2
;
Result
NUMBER3 BIGINTNUMBER1 BIGINTNUMBER2 BIGINT
312

Function chaining

Instead of nesting function calls like LOWER(TRIM(' HELLO ')), you can chain them left-to-right using dot notation. The rule: expr.FUNC(args) becomes FUNC(expr, args) — the expression before the dot is inserted as the first argument.

FeatureQL
RETURN
    (1 + 2).MULTIPLY(3), -- = MULTIPLY((1+2), 3)
    (1).ADD(2).MULTIPLY(3), -- = MULTIPLY(ADD(1, 2), 3)
    (1).ADD(2).MULTIPLY(3)::DOUBLE.SQRT()::BIGINT, -- You can also use casting
    ' HELLO '.TRIM().LOWER() -- = LOWER(TRIM(' HELLO '))
;
Result
?_0 BIGINT?_1 BIGINT?_2 BIGINT?_3 VARCHAR
993hello

Chains read left-to-right: (1).ADD(2).MULTIPLY(3) means MULTIPLY(ADD(1, 2), 3). After a cast, both ::DOUBLE.SQRT() and ::(DOUBLE).SQRT() work. Prefer the parenthesized form ::(DOUBLE).SQRT() when a type name could be read as a namespace (DOUBLE.SQRT() looks like a dotted reference).

Chaining on namespaced features

Features can have dotted names like FM.MYFEATURES.FEATURE1. This is never ambiguous: the last segment before the opening parenthesis is always the function call, so FM.MYFEATURES.FEATURE1.ADD(2) can only mean "call ADD on feature FM.MYFEATURES.FEATURE1":

FeatureQL
RETURN
    feature1 := 1,
    feature1.ADD(2),
    fm.myfeatures.feature1 := 1,
    fm.myfeatures.feature1.ADD(2)
;
Result
FEATURE1 BIGINT?_1 BIGINTFM.MYFEATURES.FEATURE1 BIGINT?_3 BIGINT
1313

Chaining with namespaced functions

When the function itself has a dotted name (like macro FM.FUNCTIONS.AREA), wrap it in double-quotes so FeatureQL can tell where the feature ends and the function begins. Without quotes, SOME.LENGTH.FM.FUNCTIONS.AREA(SOME.WIDTH) is genuinely ambiguous — it could be feature SOME.LENGTH.FM.FUNCTIONS calling AREA, or feature SOME.LENGTH calling FM.FUNCTIONS.AREA. Quoting resolves it: SOME.LENGTH."FM.FUNCTIONS.AREA"(SOME.WIDTH).

FeatureQL
DEFINE
    param1 := INPUT(BIGINT),
    param2 := INPUT(BIGINT),
    fm.functions.area := MACRO(param1 * param2 USING param1, param2)
RETURN
    some.length := 2,
    some.width := 3,
    "FM.FUNCTIONS.AREA"(some.length, some.width), -- Standard call
    some.length."FM.FUNCTIONS.AREA"(some.width) -- Chained: SOME.LENGTH becomes first arg
;
Result
SOME.LENGTH BIGINTSOME.WIDTH BIGINT?_2 BIGINT?_3 BIGINT
2366

Syntax flexibility

Alternative assignment syntax

FeatureQL preferred syntax is := for assignment but the SQL-style expr AS name works, too.

The := form puts the name first, which many people find easier to scan:

FeatureQL
RETURN
    -- ** Traditional SQL way **
    1 AS NUMBER1,
    NUMBER1 + 1 AS NUMBER2,
    -- ** FeatureQL assignment style **
    NUMBER3 := 1,
    NUMBER4 := NUMBER3 + 1,
Result
NUMBER1 BIGINTNUMBER2 BIGINTNUMBER3 BIGINTNUMBER4 BIGINT
1212

Flexible formatting

Trailing commas, arbitrary whitespace, and multi-line expressions are all valid:

FeatureQL
RETURN
    number1 := 1, -- Arbitrary whitespaces
    number2 := nuMbeR1 + 1, -- Case insensitive
    number3 := ' 2 '
        .TRIM()
        ::BIGINT
        .ADD(1), -- Chaining on multi-lines
    array_of_ints := ARRAY[
        1,
        2,
        3, -- Trailing comma in arrays and rows
    ],
    number4 := number2 + 2, -- Trailing comma in feature list
Result
NUMBER1 BIGINTNUMBER2 BIGINTNUMBER3 BIGINTARRAY_OF_INTS ARRAY<BIGINT>NUMBER4 BIGINT
123[1, 2, 3]4

Trailing commas mean you can reorder features without worrying about which one is last. Multi-line chaining (.TRIM() on one line, ::BIGINT on the next) keeps complex expressions readable.

Function aliases

SQL dialects use different names for the same operation. FeatureQL accepts common aliases — ARRAY and LIST, POWER and POW — so you can use the names you already know. For slicing, write SLICE_LEN (length) or SLICE_END (inclusive end); ARRAY_SLICE and LIST_SLICE are rejected because dialects disagree on those names.

FeatureQL
RETURN
    ARRAY[1, 2, 3, 4, 5, 6] AS ARRAY_1,
    ARRAY(1, 2, 3, 4, 5, 6) AS ARRAY_2, -- For Array and Row, you can use brackets or parentheses
    SLICE(ARRAY_1, 2, 3) AS SLICED_1,
    SLICE_LEN(ARRAY_1, 2, 3) AS SLICED_2, -- SLICE_LEN is the explicit length name
    SLICE_END(ARRAY_2, 2, 4) AS SLICED_3 -- SLICE_END is inclusive end, same as [2:4]
;
Result
ARRAY_1 ARRAY<BIGINT>ARRAY_2 ARRAY<BIGINT>SLICED_1 ARRAY<BIGINT>SLICED_2 ARRAY<BIGINT>SLICED_3 ARRAY<BIGINT>
[1, 2, 3, 4, 5, 6][1, 2, 3, 4, 5, 6][2, 3, 4][2, 3, 4][2, 3, 4]

Beyond name aliases, constructors also accept both brackets and parentheses: ARRAY[1,2,3] and ARRAY(1,2,3) are equivalent, as are ROW(...) and ROW[...].

FeatureQL normalizes aliases to a single canonical name when features are saved, so stored definitions stay consistent regardless of which alias you used.

On real data

Foundations so far used literals. A feature that reads a source follows one shape: declare columns, bind a key to an INPUT(), and name the source. INLINE_COLUMNS() embeds the rows in the query so you can try it without a warehouse:

FeatureQL
DEFINE
    customer_id := INPUT(BIGINT),
    dim_customers := INLINE_COLUMNS(
        customer_id BIGINT BIND TO customer_id,
        name VARCHAR
        FROM CSV(
            customer_id,name
            101,Alice
            102,Bob
            103,Charly
        )
    )
RETURN
    customer_id,
    customer_name := dim_customers[name]
FOR
    customer_id := BIND_VALUES(ARRAY(101, 102, 103))
;
Result
CUSTOMER_ID BIGINTCUSTOMER_NAME VARCHAR
101Alice
102Bob
103Charly

EXTERNAL_COLUMNS(... FROM TABLE(...)) uses the same column list against a real table. Full source types: Mapping to data sources .

Next

Query structure — DEFINE, RETURN, FOR, WHERE.