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.
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)
;| ARG DOUBLE | F DOUBLE | G DOUBLE | F_OF_G DOUBLE | G_OF_F DOUBLE |
|---|---|---|---|---|
| 5 | 10 | 105 | 210 | 110 |
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.
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)))
;| ORIGINAL DOUBLE | SWAPPED DOUBLE | SINGLE_PASS DOUBLE |
|---|---|---|
| 7 | -7 | 12 |
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.
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)
;| ARG DOUBLE | FIRST_VARIANT DOUBLE | SECOND_VARIANT DOUBLE |
|---|---|---|
| 5 | 210 | 24 |
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.
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)
;Entity annotations follow the replacement in the same way:
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)
;| BASE_TYPE VARCHAR | SWAPPED_TYPE VARCHAR |
|---|---|
| BIGINT#CUSTOMERS | BIGINT#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:
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);| ARG DOUBLE | CHOSEN DOUBLE |
|---|---|
| 5 | 105 |
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.