Composition and substitution

The unit of definition, reuse and validation is a single named calculation. It is not a table, a model, or a metric under a framework's aggregation rules. Any feature can reference any other feature, across entities, and the compiler resolves the dependency graph.

Reuse by name

A persisted feature is used by name. FROM fm.namespace brings a namespace into scope, and the registry resolves sources and dependencies.

-- Defined once, in the registry
CUSTOMER_AT_RISK := HAS_OPEN_TICKET OR EXCEEDS_SLA

-- Reused at the subscription grain
SUB_CUSTOMER_IS_HEALTHY := NOT RELATED(CUSTOMER_AT_RISK VIA SUB_CUSTOMER_ID)

-- Reused as an API
CREATE FEATURE HEALTH_PS AS PREPARED_STATEMENT(HEALTH_STATUS USING CUSTOMER_ID)

-- An alternative, with the original left in place
VARIANT(CUSTOMER_AT_RISK REPLACING PAYMENT_SLA_DAYS WITH 30.0)

None of the four uses restates the rule.

Publishing

FeatureQL has no PUBLIC or PRIVATE keyword. With CREATE FEATURES AS, every feature in the PERSIST part becomes addressable: others can reference it and replace it in a variant. Features in the DEFINE part are persisted as dependencies. They are visible through SHOW CREATE FEATURES, and nobody can build on them. See Create features .

VARIANT: substitution on the dependency graph

VARIANT(F REPLACING D WITH D') produces a new computation in which D is replaced by D' everywhere in the dependencies of F. Other features that share D keep the original. A feature is identified by its name and the hash of its dependency tree, so a variant is a distinct feature and the base keeps running unchanged.

A feature with a free INPUT is already a function of that input. VARIANT supplies the argument. Each REPLACING origin must be in the graph of the base, and replacing the base with itself is an error (UE/VARIANT-ORIGIN-NOT-A-DEPENDENCY).

Substitution has the properties expected of a substitution operator.

It composes.

FeatureQL
DEFINE
    arg := INPUT(DOUBLE),
    f := arg * 2e0,
    g := arg + 100e0,
    f_alias := f,
    g_alias := g,
    f_after_g := VARIANT(f_alias REPLACING arg WITH g),
    g_after_f := VARIANT(g_alias REPLACING arg WITH f)
RETURN
    arg,
    f,
    g,
    f_of_g := f_after_g,
    g_of_f := g_after_f
FOR
    arg := BIND_VALUE(5e0)
;
Result
ARG DOUBLEF DOUBLEG DOUBLEF_OF_G DOUBLEG_OF_F DOUBLE
510105210110

It is parallel and single-pass. Replacing two dependencies at once swaps them. Sequential replacement would give 0: A by B gives B - B, then B by A gives A - A. The result is -7. Each replacement value is computed in the original graph, so it may also mention the feature it replaces, without looping.

FeatureQL
DEFINE
    a := INPUT(DOUBLE),
    b := INPUT(DOUBLE),
    base := a - b,
    basea := base,
    swap := VARIANT(basea REPLACING a, b WITH b, a),
    arg := INPUT(DOUBLE),
    inner := arg * 2e0,
    repl := arg + 1e0,
    innera := inner,
    single_pass := VARIANT(innera REPLACING arg WITH repl)
RETURN
    original := base,
    swapped := swap,
    single_pass
FOR
    (a, b, arg) := BIND_VALUES(ARRAY(ROW(10e0, 3e0, 5e0)))
;
Result
ORIGINAL DOUBLESWAPPED DOUBLESINGLE_PASS DOUBLE
7-712

It nests. A variant of a variant substitutes into the graph that was already substituted. An origin that the inner variant replaced is gone from that graph, so replacing it again is an error. Write one variant on the original base that lists every replacement instead. Allowing the second replacement would make the result depend on which layer wins, and the single variant already expresses it.

FeatureQL
DEFINE
    arg := INPUT(DOUBLE),
    f := arg * 2e0,
    g := arg + 100e0,
    h := arg + 7e0,
    fa := f,
    v1 := VARIANT(fa REPLACING arg WITH g),
    v1a := v1,
    v2 := VARIANT(v1a REPLACING g WITH h)
RETURN
    arg,
    first_variant := v1,
    second_variant := v2
FOR
    arg := BIND_VALUE(5e0)
;
Result
ARG DOUBLEFIRST_VARIANT DOUBLESECOND_VARIANT DOUBLE
521024

The rewritten graph is type-checked again. A length injected into a currency addition is refused. The feature named in the message is the substituted node.

FeatureQL
DEFINE
    usd_a := INPUT(DECIMAL(10,2)@CURRENCY.USD),
    usd_b := INPUT(DECIMAL(10,2)@CURRENCY.USD),
    meters := INPUT(DECIMAL(10,2)@LENGTH.METER),
    base := usd_a + usd_b,
    basea := base,
    bad := VARIANT(basea REPLACING usd_b WITH meters)
RETURN
    base_type := TYPEOF(base),
    bad_type := TYPEOF(bad)
FOR
    usd_a := BIND_VALUE(1.00),
    usd_b := BIND_VALUE(2.00),
    meters := BIND_VALUE(3.00)
;
Result
ERROR UE/UNIT-LABEL-MISMATCH Unit labels must match for ADD(); got USD and m. Annotate or convert one operand so both share the same unit when defining feature BASE__CTX_USD_B_METERS.

Entity annotations follow the replacement in the same way:

FeatureQL
DEFINE
    customers := ENTITY(),
    orders := ENTITY(),
    cid := INPUT(BIGINT#customers),
    oid := INPUT(BIGINT#orders),
    base := cid,
    basea := base,
    swapped := VARIANT(basea REPLACING cid WITH oid)
RETURN
    base_type := TYPEOF(base),
    swapped_type := TYPEOF(swapped)
FOR
    cid := BIND_VALUE(1),
    oid := BIND_VALUE(2)
;
Result
BASE_TYPE VARCHARSWAPPED_TYPE VARCHAR
BIGINT#CUSTOMERSBIGINT#ORDERS

See Variants .

Macros and UDFs

MACRO(expr USING x, y) abstracts an expression over its inputs. A user-defined function (UDF) adds a name with typed signatures and an IMPLEMENTED AS body per backend dialect (DUCKDB, TRINO, DATAFUSION). Serving uses DATAFUSION. There is no default body.

Neither is a value. A macro cannot be passed to a macro or stored in an array. Macros are expanded before the graph is built, so a macro is not a node of the graph and cannot be the origin of a VARIANT. A call is a variant of the body that replaces the parameters with the arguments, so the substitution rules above apply to calls too.

First order, with selection by name at compile time

There is no function type. A feature cannot take a function as a parameter. Which feature is used can be selected at compile time, by name:

FeatureQL
CONST FN := 'G'
DEFINE
    ARG := INPUT(DOUBLE),
    F   := ARG * 2e0,
    G   := ARG + 100e0,
RETURN
    ARG,
    CHOSEN := @eval_as_text(FN)
FOR ARG := BIND_VALUE(5e0);
Result
ARG DOUBLECHOSEN DOUBLE
5105

The runtime is first order, and compile-time evaluation can select among feature names. Closures have no SQL equivalent, so this is where the language stops. See Metaprogramming .

Limits

  • A graph of several hundred features is not easier to navigate than a long SQL file by itself. Each node is named, typed and inspectable. Navigation relies on queries such as SHOW FEATURES WHERE IS_DOWNSTREAM_OF(...).
  • Variants multiply in a large organization. Retiring them is a convention to set, not something the language does.