Metaprogramming
FeatureQL is a pure functional language — every feature is a deterministic function of its inputs. But sometimes you need to compute a value at compile time and inject it into the query before execution. That's what metaprogramming does.
Traditional SQL typically uses Jinja templates for this. FeatureQL provides two native functions instead: @eval_as_text() and @eval_as_literal().
@eval_as_text() — inject text into the query
@eval_as_text() evaluates an expression at compile time and splices the result as raw text into the query. The expression must evaluate to VARCHAR.
@eval_as_text() is useful when you need to construct identifiers or query fragments dynamically. In this example, a constant string is injected both as a raw text splice (via @eval_as_text()) and as a typed literal (via @eval_as_literal()):
@fql-playground(meta_echo_literal_duplicate_constant)
If the value you want to echo isn't already a string, cast it explicitly — @eval_as_text() only accepts VARCHAR:
@fql-playground(meta_echo_varchar_with_cast_to_int)
@eval_as_literal() — inject a typed value
@eval_as_literal() evaluates an expression at compile time and injects the result as a typed literal. Unlike @eval_as_text(), it preserves the original type of the computed value.
SELECT @eval_as_literal(1+2) AS RESULT| RESULT BIGINT |
|---|
| 3 |
Use @eval_as_literal() when you need the result to participate in type checking as its native type, rather than being spliced as text.
CONST declarations
CONST defines compile-time constants that @eval_as_text() and @eval_as_literal() can reference. Several syntaxes are supported:
-- Single constant
CONST MY_VALUE := 42
-- Multiple constants on one line
CONST CONST1 := 1, CONST2 := 2
-- Multiple constants across lines
CONST
CONST1 := 1,
CONST2 := 2
-- Trailing commas are allowed
CONST
CONST1 := 1,
CONST2 := 2, Constants support complex expressions — they're evaluated before the rest of the query is compiled:
CONST BASE := 10
SELECT @eval_as_literal(BASE * 2 + 3) AS RESULT| RESULT BIGINT |
|---|
| 23 |
Persisting features with compile-time values
When you persist features that include compile-time computed parameters, use EVAL_CONST(). You must specify the type explicitly because the value is computed before type inference runs:
CREATE OR REPLACE FEATURES AS
CONST EXTERNAL_CONSTANT := 0
SELECT FM.TUTORIALS.META.CST_EXTERNAL := ADD(eval_const(2*EXTERNAL_CONSTANT AS BIGINT), 1);| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.META.CST_EXTERNAL | CREATED | Feature created as not exists |
Selecting the persisted feature returns the value that was computed at creation time:
CONST
EXTERNAL_CONSTANT := 5
SELECT
FM.TUTORIALS.META.CST_EXTERNAL| FM.TUTORIALS.META.CST_EXTERNAL BIGINT |
|---|
| 11 |
Non-deterministic functions
RANDOM() and CURRENT_TIMESTAMP() return different values on each invocation, which violates FeatureQL's pure function model. Using them directly produces an error — you must wrap them in @eval_as_text() or @eval_as_literal() so the value is evaluated once at compile time and stays constant for all rows.
Here, CURRENT_TIMESTAMP() is evaluated once and every row gets the same timestamp:
WITH
ID := INPUT(BIGINT),
SELECT
ID := BIND_VALUES(ARRAY[1,2,3]),
RESULT := @eval_as_literal(CURRENT_TIMESTAMP())| ID BIGINT | RESULT VARCHAR |
|---|---|
| 1 | 2025-09-13 10:22:02 |
| 2 | 2025-09-13 10:22:02 |
| 3 | 2025-09-13 10:22:02 |
The same approach works for RANDOM() — every row gets the same random value:
WITH
ID := INPUT(BIGINT),
SELECT
ID := BIND_VALUES(ARRAY[1,2,3]),
RESULT := @eval_as_literal(RANDOM())| ID BIGINT | RESULT VARCHAR |
|---|---|
| 1 | 0.5523897924574934 |
| 2 | 0.5523897924574934 |
| 3 | 0.5523897924574934 |
Per-row deterministic randomness
If you need values that vary per row but remain consistent across executions, use HASH01(). It generates a deterministic value in [0, 1] based on the hash of its input:
WITH
ID := INPUT(BIGINT),
SELECT
ID := BIND_VALUES(ARRAY[1,2,3]),
RESULT := HASH01(ID::VARCHAR)| ID BIGINT | RESULT VARCHAR |
|---|---|
| 1 | 0.7687112224818731 |
| 2 | 0.7817145915735615 |
| 3 | 0.9249844845138457 |
Randomness that changes each execution
To get per-row values that also change between executions, combine HASH01() with a compile-time random seed. The seed changes each run, producing different per-row values:
WITH
ID := INPUT(BIGINT),
SELECT
ID := BIND_VALUES(ARRAY[1,2,3]),
RESULT := HASH01(ID::VARCHAR || @eval_as_literal(RANDOM()::VARCHAR))| ID BIGINT | RESULT VARCHAR |
|---|---|
| 1 | 0.8223445174536488 |
| 2 | 0.37217093613676144 |
| 3 | 0.4197024910057665 |
When to use @eval_as_text() vs @eval_as_literal()
| Scenario | Use | Why |
|---|---|---|
| Inject a computed number, date, or array | @eval_as_literal() | Preserves the native type |
| Construct a feature name or SQL fragment dynamically | @eval_as_text() | Produces raw text |
Wrap RANDOM() or CURRENT_TIMESTAMP() | Either | @eval_as_literal() is usually cleaner |
| Persist a compile-time value in a feature definition | EVAL_CONST() | Designed for persistence; requires explicit type |