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: @echo() and @literal().
@echo() — inject text into the query
@echo() evaluates an expression at compile time and splices the result as raw text into the query. The expression must evaluate to VARCHAR.
@echo() 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 @echo()) and as a typed literal (via @literal()):
CONST MY_TEXT := 'hello'
SELECT '@echo(MY_TEXT)' AS RESULT1, @literal(MY_TEXT) AS RESULT2| RESULT1 VARCHAR | RESULT2 VARCHAR |
|---|---|
| hello | hello |
If the value you want to echo isn't already a string, cast it explicitly — @echo() only accepts VARCHAR:
CONST MY_NUMBER := 42
SELECT @echo(MY_NUMBER::VARCHAR) AS RESULT| RESULT BIGINT |
|---|
| 42 |
@literal() — inject a typed value
@literal() evaluates an expression at compile time and injects the result as a typed literal. Unlike @echo(), it preserves the original type of the computed value.
SELECT @literal(1+2) AS RESULT| RESULT BIGINT |
|---|
| 3 |
Use @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 @echo() and @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 @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 FEATURE FM.TUTORIALS.META.CST_EXTERNAL AS ADD(eval_const('2*EXTERNAL_CONSTANT', TYPE '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 @echo() or @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 := @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 := @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 || @literal(RANDOM()::VARCHAR))| ID BIGINT | RESULT VARCHAR |
|---|---|
| 1 | 0.8223445174536488 |
| 2 | 0.37217093613676144 |
| 3 | 0.4197024910057665 |
When to use @echo() vs @literal()
| Scenario | Use | Why |
|---|---|---|
| Inject a computed number, date, or array | @literal() | Preserves the native type |
| Construct a feature name or SQL fragment dynamically | @echo() | Produces raw text |
Wrap RANDOM() or CURRENT_TIMESTAMP() | Either | @literal() is usually cleaner |
| Persist a compile-time value in a feature definition | EVAL_CONST() | Designed for persistence; requires explicit type |