Variants
VARIANT() creates a new version of a feature by replacing specific dependencies with alternative definitions. The original feature and all its other dependencies stay unchanged — only the parts you specify get swapped.
This is the core mechanism for experimentation and what-if analysis in FeatureQL. Instead of duplicating an entire computation pipeline to test one change, you replace a single dependency and let the system rebuild the rest.
Syntax
VARIANT(
feature
REPLACING feature1, feature2, ...
WITH replacement1, replacement2, ...
) The REPLACING clause names the features to swap out; WITH provides the new definitions in the same order.
Four rules govern every variant:
- Each origin must be a dependency of the base. Replacing a feature the base does not use, or the base itself, is an error (
UE/VARIANT-ORIGIN-NOT-A-DEPENDENCY). - Replacements are simultaneous.
REPLACING A, B WITH B, Aswaps the two. A replacement value is computed with the original features: the other replacements of the sameVARIANT()do not apply to it, and a value may mention the feature it replaces (REPLACING X WITH X + 1) without looping. - Only the base's graph changes. Features outside the base keep the original dependencies, even when they share them.
- The rewritten graph is type-checked again. Replacing a
BIGINTinput with aDOUBLEmakes every rewritten feature downstream aDOUBLE.
The composition guide explains why substitution works this way.
Basic usage
The simplest use of VARIANT() is evaluating a feature with different hardcoded inputs. Here, OPERATION computes POW(EXPR, POWER), and two variants evaluate it with different values:
DEFINE
expr := INPUT(BIGINT),
power := INPUT(BIGINT),
operation := POW(expr, power)
RETURN
result1 := VARIANT(operation REPLACING expr, power WITH 1, 2),
result2 := VARIANT(operation REPLACING expr, power WITH 3, 4)
;| RESULT1 BIGINT | RESULT2 BIGINT |
|---|---|
| 1 | 81 |
You can also include the original feature alongside its variants. This is useful for comparing a baseline against alternatives:
DEFINE
expr := INPUT(BIGINT),
power := INPUT(BIGINT),
operation := POW(expr, power)
RETURN
result1 := VARIANT(operation REPLACING expr, power WITH 1, 2),
result2 := VARIANT(operation REPLACING expr, power WITH 3, 4),
operation
FOR
expr := BIND_VALUE(5),
power := BIND_VALUE(6)
;| RESULT1 BIGINT | RESULT2 BIGINT | OPERATION BIGINT |
|---|---|---|
| 1 | 81 | 15625 |
Replacing with expressions
Replacements aren't limited to literals — you can use any feature or expression. Here, EXPR2 is a named feature used as a replacement value, and it can appear in multiple variants:
DEFINE
expr := INPUT(BIGINT),
power := INPUT(BIGINT),
operation := POW(expr, power)
RETURN
expr2 := 2,
result1 := VARIANT(operation REPLACING expr, power WITH 2, 2),
result2 := VARIANT(operation REPLACING expr, power WITH 2, expr2),
result3 := VARIANT(operation REPLACING expr, power WITH expr2, expr2)
;| EXPR2 BIGINT | RESULT1 BIGINT | RESULT2 BIGINT | RESULT3 BIGINT |
|---|---|---|---|
| 2 | 4 | 4 | 4 |
Replacing formulas, not just inputs
VARIANT() replaces dependencies, not the feature itself. Replacing the base with itself is rejected. To swap the formula, create an alias and replace the alias's dependency:
DEFINE
expr := INPUT(BIGINT),
power := INPUT(BIGINT),
operation_base := POW(expr, power),
operation_replacement := expr + power,
operation_alias := operation_base
RETURN
expr,
power,
result2 := VARIANT(
operation_alias REPLACING operation_base WITH operation_replacement
)
FOR
expr := BIND_VALUE(5),
power := BIND_VALUE(6)
;| EXPR BIGINT | POWER BIGINT | RESULT2 BIGINT |
|---|---|---|
| 5 | 6 | 11 |
RESULT2 works because OPERATION_ALIAS depends on OPERATION_BASE, so the replacement takes effect.
Binding through variants
You can't use BIND_VALUES() directly inside a VARIANT() replacement. Instead, bind to a separate input feature and use that feature as the replacement:
DEFINE
expr := INPUT(BIGINT),
power := INPUT(BIGINT),
operation := POW(expr, power),
bound_feature := INPUT(BIGINT)
RETURN
expr2 := 2,
result1 := VARIANT(operation REPLACING expr, power WITH 2, bound_feature)
FOR
bound_feature := BIND_VALUES(ARRAY(1, 2, 3))
;| EXPR2 BIGINT | RESULT1 BIGINT |
|---|---|
| 2 | 2 |
| 2 | 4 |
| 2 | 8 |
This produces multiple rows because BOUND_FEATURE is bound to three values, and the variant inherits that binding.
Shared dependencies
When a variant replaces a dependency that's shared with other features, each feature gets its own version of the dependency chain. Consider features A and B that both depend on C, which depends on D:
A := FUNC(C),
B := FUNC(C),
C := FUNC(D, E)
Creating VARIANT(A REPLACING D WITH D_new) produces:
A_variantuses a modifiedCthat referencesD_newinstead ofDBcontinues using the originalCwith the originalD
The two features no longer share the same C. For caching and lineage tracking, each feature is identified by its name plus a hash of its entire dependency tree — not just its name.
Nested variants
A variant of a variant substitutes into the graph that the inner variant already rewrote. Both sets of replacements apply:
DEFINE
price := INPUT(BIGINT),
qty := INPUT(BIGINT),
revenue := price * qty,
with_price := VARIANT(revenue REPLACING price WITH 10)
RETURN
revenue,
with_price,
with_price_and_qty := VARIANT(with_price REPLACING qty WITH 3)
FOR CROSS
price := BIND_VALUE(5),
qty := BIND_VALUE(2)
;| REVENUE BIGINT | WITH_PRICE BIGINT | WITH_PRICE_AND_QTY BIGINT |
|---|---|---|
| 10 | 20 | 30 |
WITH_PRICE has replaced PRICE, so PRICE is no longer in its graph. A second VARIANT(WITH_PRICE REPLACING PRICE WITH 7) is therefore rejected with UE/VARIANT-ORIGIN-NOT-A-DEPENDENCY. To change both values, write one variant on the original base: VARIANT(REVENUE REPLACING PRICE, QTY WITH 7, 3). The one exception is an origin that an inner replacement value still uses: after REPLACING X WITH X + 1, an outer REPLACING X WITH 3 applies inside X + 1.
A replacement value can itself be a variant. Here the two variants of OPERATION_FINAL replace OPERATION_2 with different alternatives — one that changes the multiplier, and one that is a variant of OPERATION_2 that changes both the base expression and the exponent:
DEFINE
expr1 := INPUT(BIGINT),
expr2 := 2,
expr3 := 3,
expr4 := 4,
operation_1 := expr1 + expr2,
operation_2 := operation_1 * expr4, -- OPERATION_2 = (EXPR1 + EXPR2) * EXPR4
operation_3 := POW(operation_2, expr4),
operation_4 := expr1 - expr2,
operation_final := operation_3 + operation_1, -- OPERATION_FINAL := POW((EXPR1 + EXPR2) * EXPR4, EXPR4) + (EXPR1 + EXPR2),
operation_2_replacement1 := operation_1 * expr3, -- OPERATION_2 = (EXPR1 + EXPR2) * EXPR3
operation_2_replacement2 := VARIANT(
operation_2 REPLACING operation_1, expr4 WITH operation_4, 5
), -- OPERATION_2 = (EXPR1 - EXPR2) * 5
RETURN
expr1,
operation_alias1 := VARIANT(
operation_final REPLACING operation_2 WITH operation_2_replacement1
), -- OPERATION_ALIAS1 := POW((EXPR1 + EXPR2) * EXPR3, EXPR4) + (EXPR1 + EXPR2),
operation_alias2 := VARIANT(
operation_final REPLACING operation_2 WITH operation_2_replacement2
), -- OPERATION_ALIAS2 := POW((EXPR1 - EXPR2) * 5, EXPR4) + (EXPR1 + EXPR2),
FOR
expr1 := BIND_VALUES(ARRAY(1, 2, 3))
;| EXPR1 BIGINT | OPERATION_ALIAS1 BIGINT | OPERATION_ALIAS2 BIGINT |
|---|---|---|
| 1 | 6564 | 628 |
| 2 | 20740 | 4 |
| 3 | 50630 | 630 |
When to use variants vs macros
Both VARIANT() and MACRO() enable reuse, but they serve different purposes:
VARIANT() | MACRO() | |
|---|---|---|
| Purpose | Create alternative versions of existing features | Create reusable function templates |
| Mechanism | Replaces dependencies in an existing DAG | Parameterizes a feature expression |
| Best for | Experimentation, A/B testing, what-if analysis | Reusable calculations, DRY patterns |
| Scope | Affects the entire dependency chain | Self-contained — no side effects on other features |
See Macros for details on creating reusable function templates.