Binding inputs
Features defined with INPUT() are placeholders — they declare what a feature needs, not what value it has. Binding is how you provide those concrete values so the query can actually execute.
Write bindings in the FOR clause. DEFINE and RETURN also accept BIND_VALUE / BIND_VALUES, and FORMAT (FIX) moves those to FOR. Use FOR in new queries.
FeatureQL offers several binding operators depending on where your data comes from:
| Operator | What it does | Typical use case |
|---|---|---|
BIND_VALUE() | Binds a single value | Testing, simple parameterization |
BIND_VALUES() | Binds an array of values | Prototyping, small multi-value evaluation |
BIND_COLUMNS() | Binds columns from a table or query | Large or table-backed populations |
@BIND_KEYSET() | Binds entity keys from a predefined set | Reusable population selection |
How binding affects result cardinality
The number of rows you get back depends on how many values you bind and whether you bind features together or separately:
- Single feature, n values → n rows.
- Multiple features, single binding (using
ROW()): n value pairs → n rows. The values are paired positionally. - Multiple features, separate bindings (requires
FOR CROSS): n values × m values → n × m rows. FeatureQL produces the Cartesian product of all combinations.
Bind a single value
The simplest case: one value per input. Derived features are computed automatically from the bound values.
DEFINE
name := INPUT(VARCHAR), -- Declare an input of type VARCHAR
hello := 'Hello, ' || name || '!',
number := INPUT(BIGINT), -- Declare an input of type BIGINT
other_number := number * 2
RETURN
name,
hello,
number,
other_number
FOR
name := BIND_VALUE('FeatureQL'),
number := BIND_VALUE(5)
;| NAME VARCHAR | HELLO VARCHAR | NUMBER BIGINT | OTHER_NUMBER BIGINT |
|---|---|---|---|
| FeatureQL | Hello, FeatureQL! | 5 | 10 |
Bind multiple values
Bind an array of values to evaluate a feature across several inputs at once.
One feature
Pass an array to BIND_VALUES(). Each element produces one result row.
DEFINE
input1 := INPUT(BIGINT),
calculated := input1 + 1
RETURN
input1,
calculated
FOR
input1 := BIND_VALUES(ARRAY(1, 3, 5))
;| INPUT1 BIGINT | CALCULATED BIGINT |
|---|---|
| 1 | 2 |
| 3 | 4 |
| 5 | 6 |
Multiple features, paired
To bind several features together, wrap each set of values in a ROW(). The first row goes with the first row, second with second, and so on — no cross-product.
DEFINE
input1 := INPUT(BIGINT),
input2 := INPUT(BIGINT),
calculated := input1 + input2
RETURN
input1,
input2,
calculated
FOR
(input1, input2) := BIND_VALUES(ARRAY(ROW(1, 2), ROW(3, 4), ROW(5, 6)))
;| INPUT1 BIGINT | INPUT2 BIGINT | CALCULATED BIGINT |
|---|---|---|
| 1 | 2 | 3 |
| 3 | 4 | 7 |
| 5 | 6 | 11 |
Multiple features, separate (Cartesian product)
When you bind features with separate BIND_VALUES() calls, FeatureQL evaluates every combination. Three values for each of two features produces 3 × 3 = 9 rows.
Because cross products can produce unexpectedly large result sets, FeatureQL requires the explicit FOR CROSS keyword instead of plain FOR. This makes the intent clear and prevents accidental Cartesian products when you meant to pair values. If you want pairing instead, use ROW() tuples in a single BIND_VALUES() as shown above.
DEFINE
featuref1 := INPUT(BIGINT),
featuref2 := INPUT(BIGINT),
featureff := featuref1 + featuref2
RETURN
featuref1,
featuref2,
featureff
FOR CROSS
featuref1 := BIND_VALUES(ARRAY(1, 3, 5)),
featuref2 := BIND_VALUES(ARRAY(2, 4, 6))
;| FEATUREF1 BIGINT | FEATUREF2 BIGINT | FEATUREFF BIGINT |
|---|---|---|
| 1 | 2 | 3 |
| 1 | 4 | 5 |
| 1 | 6 | 7 |
| 3 | 2 | 5 |
| 3 | 4 | 7 |
| 3 | 6 | 9 |
| 5 | 2 | 7 |
| 5 | 4 | 9 |
| 5 | 6 | 11 |
Bind to external data
When your input values live in a database table, use BIND_COLUMNS() to pull them in directly. First, let's create a sample table:
CREATE SCHEMA IF NOT exists bnd;
--
DROP TABLE IF EXISTS bnd.table_example;
--
CREATE TABLE bnd.table_example AS
SELECT col_key, col_key + 1 AS val1, col_key * 2 AS val2
FROM (VALUES (1), (2), (3)) AS t(col_key);
--
SELECT CAST(COUNT() AS INTEGER) AS Count FROM bnd.table_example;| Count BIGINT |
|---|
| 3 |
Then bind columns from that table to features. The query evaluates once per row in the source table.
DEFINE
input1 := INPUT(BIGINT),
input2 := INPUT(BIGINT),
add_inputs := input1 + input2
RETURN
input1,
input2,
add_inputs
FOR
(input1, input2) := BIND_COLUMNS(
val1,
val2
FROM TABLE(bnd.table_example)
)
;| INPUT1 BIGINT | INPUT2 BIGINT | ADD_INPUTS BIGINT |
|---|---|---|
| 2 | 2 | 4 |
| 3 | 4 | 7 |
| 4 | 6 | 10 |
Nested bindings for multi-entity queries
Grain is the entity level a row represents — one customer, one order, one line item. Outer grain is what RETURN returns; a nested grain is a finer entity used only inside RELATED() / EXTEND().
When a query computes features at one entity grain but those features depend on data from finer-grained entities, you need values for the sub-entity keys too. Use the NESTED keyword to supply those sub-entity bindings without affecting the outer result cardinality.
FOR
CUSTOMER_ID := BIND_COLUMNS(id FROM TABLE(dim_customers)), -- outer grain: one row per customer
NESTED ORDER_ID := BIND_COLUMNS(id FROM TABLE(fct_orders)), -- sub-entity: all order keys
NESTED ITEM_ID := BIND_COLUMNS(id FROM TABLE(fct_items)), -- sub-entity: all item keys
The outer binding (CUSTOMER_ID) drives the result rows. The NESTED bindings provide the key pools that the RELATED() and EXTEND() subqueries inside each outer row can look up.
What counts as a sub-entity key
A NESTED binding is appropriate when the INPUT() feature is only consumed inside a RELATED() or EXTEND() subquery, or as the BIND TO join key of an EXTERNAL_COLUMNS() mapping. If you bind such an input directly with a non-NESTED binding, FeatureQL would cross-product it with the outer entity — producing one result row per customer–order pair instead of one per customer.
NESTED bindings and FOR CROSS
NESTED bindings do not count toward the Cartesian-product requirement. A query with one non-nested BIND_COLUMNS and two NESTED bindings uses plain FOR, not FOR CROSS:
FOR -- plain FOR: only one non-nested binding
CUSTOMER_ID := BIND_COLUMNS(...),
NESTED ORDER_ID := BIND_COLUMNS(...),
NESTED TICKET_ID := BIND_COLUMNS(...),
FOR CROSS is only required when there are two or more non-nested multi-value bindings that would form a cross product.
Using persisted library features with nested sub-entities
When your namespace contains persisted features that use RELATED() (e.g. CUSTOMER_AVG_DAYS_PAID, CUSTOMER_OPEN_TICKET_COUNT), those features internally reference sub-entity keys such as INVOICE_ID and TICKET_ID. The outer RETURN only returns customer-grain features, but the sub-entity key pools still need to be provided:
RETURN
CUSTOMER_ID,
STATUS := CUSTOMER_HEALTH_STATUS -- persisted feature; depends on INVOICE_ID and TICKET_ID internally
FROM FM.MY_NAMESPACE
FOR
CUSTOMER_ID := BIND_COLUMNS(id FROM TABLE(dim_customers)),
NESTED INVOICE_ID := BIND_COLUMNS(id FROM TABLE(fct_invoices)),
NESTED TICKET_ID := BIND_COLUMNS(id FROM TABLE(fct_support_tickets)),
FeatureQL detects that INVOICE_ID and TICKET_ID are only consumed inside RELATED/EXTEND subqueries and treats them as sub-grain join keys — not as outer-grain inputs.
Using sets of keys
For a reusable population, define a KEYSET(entity, query) (the persisted feature name is the identity) and pin it with @BIND_KEYSET(keyset_feature) — or bind a KEYSET_GROUP / direct EXTERNAL_COLUMNS source. This is useful whenever you want to evaluate features for all entities matching business criteria; whether that population is suitable for an endpoint depends on its size and latency budget.
More details in Bind keyset .