Purity and determinism
Every feature is a deterministic function of its inputs: same inputs, same source data, same backend, same result. A feature has no side effects. It cannot write, log, call out or read the clock.
Where non-determinism can enter
There are two entry points. The compiler refuses NOW(), RANDOM() and similar functions anywhere else.
DEFINE
a := INPUT(BIGINT),
jitter := a + RANDOM()
RETURN
jitter
FOR
a := BIND_VALUE(1)
;Compile time. CONST definitions, @eval_as_literal() and @eval_as_text() run before the graph is built and inject frozen values. @eval_as_literal(CURRENT_TIMESTAMP()) is the time of compilation, identical on every row. The @ marks the stage in the source. See Metaprogramming .
Declared sources. EXTERNAL_COLUMNS, EXTERNAL_REDIS, EXTERNAL_HTTP and the JDBC VIEW declare where outside data enters, with types given at the boundary. Their results are inputs to the graph. See Mapping data sources .
Randomness that repeats
HASH01(key) gives a stable value in [0, 1) per key. SAMPLE_UNIFORM, SAMPLE_GAUSSIAN, SAMPLE_VALUE and the other SAMPLE_* functions build on hashes.
DEFINE
customer_id := INPUT(BIGINT),
bucket := HASH01(customer_id::VARCHAR),
in_test := bucket <.3 0.77e0
RETURN
customer_id,
in_test
FOR
customer_id := BIND_VALUES(ARRAY(1, 2, 3, 4))
ORDER BY customer_id
;| CUSTOMER_ID BIGINT | IN_TEST BOOLEAN |
|---|---|
| 1 | TRUE |
| 2 | FALSE |
| 3 | FALSE |
| 4 | TRUE |
An experiment assignment computed in batch and the same assignment computed at request time are equal, because both are a function of the key. Nothing is stored or reconciled.
Ties
Some functions pick one value among several rows. When the ordering does not determine which row wins, the result would depend on the engine. The compiler reports this as a warning (SELECTION-ARBITRARY).
DEFINE
customers := ENTITY(),
orders := ENTITY(),
customer_id := INPUT(BIGINT#customers),
order_id := INPUT(BIGINT#orders),
ord := INLINE_COLUMNS(
order_id BIGINT#orders BIND TO order_id,
customer_id BIGINT#customers,
day DATE,
channel VARCHAR
FROM JSON([
{"order_id": 101, "customer_id": 1, "day": "2026-01-02", "channel": "web"},
{"order_id": 102, "customer_id": 1, "day": "2026-01-02", "channel": "store"}
])
),
last_channel := customer_id.RELATED(
MAX_BY(ord[channel], ord[day]) GROUP BY (ord[customer_id])
),
some_channel := customer_id.RELATED(
ANY_VALUE(ord[channel]) GROUP BY (ord[customer_id])
),
channels := customer_id.RELATED(
ARRAY_AGG(ord[channel]) GROUP BY (ord[customer_id])
)
RETURN
customer_id,
last_channel,
some_channel,
channels
FOR
customer_id := BIND_VALUE(1),
NESTED order_id := BIND_VALUES(ARRAY(101, 102))
;| CUSTOMER_ID BIGINT#CUSTOMERS | LAST_CHANNEL VARCHAR | SOME_CHANNEL VARCHAR | CHANNELS ARRAY<VARCHAR> |
|---|---|---|---|
| 1 | web | web | [store, web] |
Both orders have the same day, so MAX_BY(channel, day) has two candidates. The query runs, and the warning asks for an ordering that determines the selected row.
Two other cases appear in the same block:
ANY_VALUEasks for an arbitrary value by name, so it does not warn.APPROX_DISTINCTandAPPROX_PERCENTILEstate an approximate result in the same way.ARRAY_AGGover rows has several possible element orders. The result above is in value order. AnORDER BYinsideARRAY_AGGsets the order explicitly.
No control flow
Nothing at run time decides which features exist. A CASE selects a value, and the features it references are in the graph either way. FeatureQL fixes the value. Whether the engine evaluates a branch that was not selected is decided by the engine.
Closed features
A feature is closed when no EXTERNAL_* source is reachable in its dependency graph. A closed feature can be re-evaluated anywhere, which is why TRANSFORM ... IMPORT accepts only closed definitions. RELATED, EXTEND and INPUT do not affect this. Only a link to physical data does.
What purity allows
Validation without execution. validate() returns the formatted query, the inferred output types and a backend preflight without scanning data. The registry checks every change the same way.
Safe substitution. Replacing a dependency cannot change behavior through a hidden channel. See Composition and substitution .
Laziness. An unevaluated feature is unobservable, so skipping it cannot change a result. A namespace can hold a broad library of features, and a query pays only for the ones it reaches.
Identical reruns. This is what gives a regression test on a business number its meaning.
Properties readable from the dependency graph. They can be checked without running the feature:
| If the dependency graph contains no | then the feature |
|---|---|
INPUT() | is a constant |
EXTERNAL_* source | is closed |
UNSAFE_CAST or UNSAFE aggregate | uses no escape from the semantic contracts |
Limits
Purity is a property of the computation. It says nothing about whether the source data is fresh or the source declarations are true. See the overview .