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, ...
)
sql

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, A swaps the two. A replacement value is computed with the original features: the other replacements of the same VARIANT() 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 BIGINT input with a DOUBLE makes every rewritten feature downstream a DOUBLE.

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:

FeatureQL
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)
;
Result
RESULT1 BIGINTRESULT2 BIGINT
181

You can also include the original feature alongside its variants. This is useful for comparing a baseline against alternatives:

FeatureQL
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)
;
Result
RESULT1 BIGINTRESULT2 BIGINTOPERATION BIGINT
18115625

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:

FeatureQL
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)
;
Result
EXPR2 BIGINTRESULT1 BIGINTRESULT2 BIGINTRESULT3 BIGINT
2444

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:

FeatureQL
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)
;
Result
EXPR BIGINTPOWER BIGINTRESULT2 BIGINT
5611

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:

FeatureQL
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))
;
Result
EXPR2 BIGINTRESULT1 BIGINT
22
24
28

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_variant uses a modified C that references D_new instead of D
  • B continues using the original C with the original D

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:

FeatureQL
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)
;
Result
REVENUE BIGINTWITH_PRICE BIGINTWITH_PRICE_AND_QTY BIGINT
102030

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:

FeatureQL
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))
;
Result
EXPR1 BIGINTOPERATION_ALIAS1 BIGINTOPERATION_ALIAS2 BIGINT
16564628
2207404
350630630

When to use variants vs macros

Both VARIANT() and MACRO() enable reuse, but they serve different purposes:

VARIANT()MACRO()
PurposeCreate alternative versions of existing featuresCreate reusable function templates
MechanismReplaces dependencies in an existing DAGParameterizes a feature expression
Best forExperimentation, A/B testing, what-if analysisReusable calculations, DRY patterns
ScopeAffects the entire dependency chainSelf-contained — no side effects on other features

See Macros for details on creating reusable function templates.