Mapping to data sources
Features become useful when they connect to real data. Data source mapping is how you bring external data — whether embedded in the query or stored in a database — into FeatureQL's feature graph.
Every column-based mapping follows the same pattern: declare columns, bind key columns to inputs, and specify a source via FROM. The operator and source type change, but the shape stays the same.
The general pattern
FEATURE_NAME := <OPERATOR>(
key_column TYPE BIND TO SOME_INPUT,
value_column TYPE
FROM <SOURCE>(...)
) The BIND TO clause links a key column to an INPUT() feature, enforcing a cardinality constraint: the source must return 0 or 1 row per unique input combination. If your source can return multiple rows for a given key, results will be unpredictable.
You access columns of the result using the [] operator:
CUSTOMER_NAME := FEATURE_NAME[value_column] Column-based mappings
These operators return a ROW. You pick columns from that row with [].
| Operator | Source | What it does |
|---|---|---|
INLINE_COLUMNS() | CSV(), JSON() | Embeds data directly in the query — useful for prototyping and tests |
EXTERNAL_COLUMNS() | TABLE(), SQL() | Maps to columns in external database tables or queries |
EXTERNAL_COLUMNS() | VIEW() | Maps to views in external databases (realtime serving) |
The only difference between INLINE_COLUMNS() and EXTERNAL_COLUMNS() is where the data lives. The syntax inside the parentheses is identical.
For a detailed comparison of EXTERNAL_COLUMNS() source types and when to use each, see External Columns .
Text-based mappings
These operators return a VARCHAR instead of a row. You parse the result yourself (to JSON or whatever format you need).
| Operator | What it does | Details |
|---|---|---|
EXTERNAL_REDIS() | Maps to a Redis key | See External Redis |
EXTERNAL_HTTP() | Maps to an HTTP endpoint | See External HTTP |
Inline data with CSV
The simplest way to get started: embed the data directly in your query. INLINE_COLUMNS() with CSV() lets you define a small dataset inline — no external tables needed.
WITH
CUSTOMER_ID := INPUT(BIGINT),
DIM_CUSTOMERS := INLINE_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
name VARCHAR
FROM CSV(
customer_id,name
101,Alice
102,Bob
103,Charly
104,Daniel
105,Elody
106,Francis
)
)
SELECT
CUSTOMER_ID,
DIM_CUSTOMERS,
CUSTOMER_NAME := DIM_CUSTOMERS[name]
FOR
CUSTOMER_ID := BIND_VALUES(ARRAY[101, 102, 103, 104, 105, 106]);| CUSTOMER_ID BIGINT | DIM_CUSTOMERS ROW | CUSTOMER_NAME VARCHAR |
|---|---|---|
| 101 | {customer_id: 101, name: Alice} | Alice |
| 102 | {customer_id: 102, name: Bob} | Bob |
| 103 | {customer_id: 103, name: Charly} | Charly |
| 104 | {customer_id: 104, name: Daniel} | Daniel |
| 105 | {customer_id: 105, name: Elody} | Elody |
| 106 | {customer_id: 106, name: Francis} | Francis |
Inline data with JSON
The same approach works with JSON. This is handy when your data comes from an API response or you prefer JSON syntax over CSV.
WITH
CUSTOMER_ID := INPUT(BIGINT),
DIM_CUSTOMERS := INLINE_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
name VARCHAR
FROM JSON(
[
{"customer_id": 101, "name": "Alice"},
{"customer_id": 102, "name": "Bob"},
{"customer_id": 103, "name": "Charly"},
{"customer_id": 104, "name": "Daniel"},
{"customer_id": 105, "name": "Elody"},
{"customer_id": 106, "name": "Francis"}
]
)
)
SELECT
CUSTOMER_ID,
DIM_CUSTOMERS,
CUSTOMER_NAME := DIM_CUSTOMERS[name]
FOR
CUSTOMER_ID := BIND_VALUES(ARRAY[101, 102, 103, 104, 105, 106]);| CUSTOMER_ID BIGINT | DIM_CUSTOMERS ROW | CUSTOMER_NAME VARCHAR |
|---|---|---|
| 101 | {customer_id: 101, name: Alice} | Alice |
| 102 | {customer_id: 102, name: Bob} | Bob |
| 103 | {customer_id: 103, name: Charly} | Charly |
| 104 | {customer_id: 104, name: Daniel} | Daniel |
| 105 | {customer_id: 105, name: Elody} | Elody |
| 106 | {customer_id: 106, name: Francis} | Francis |
External data with TABLE
When your data lives in a database table, use EXTERNAL_COLUMNS() with TABLE(). The example below assumes this table exists:
/* SQL */
CREATE SCHEMA IF NOT exists ext;
--
CREATE OR REPLACE TABLE ext.dim_customers AS
SELECT customer_id, name FROM (
VALUES
(101, 'Alice'),
(102, 'Bob'),
(103, 'Charly'),
(104, 'Daniel'),
(105, 'Elody'),
(106, 'Francis')
) as t(customer_id, name);| Count BIGINT |
|---|
| 6 |
With the table in place, the mapping looks almost identical to the inline version — only the source changes from CSV(...) to TABLE(schema.table):
WITH
CUSTOMER_ID := INPUT(BIGINT),
DIM_CUSTOMERS := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
name VARCHAR
FROM TABLE(ext.dim_customers)
)
SELECT
CUSTOMER_ID,
DIM_CUSTOMERS,
CUSTOMER_NAME := DIM_CUSTOMERS[name]
FOR
CUSTOMER_ID := BIND_VALUES(ARRAY[101, 102, 103, 104, 105, 106]);| CUSTOMER_ID BIGINT | DIM_CUSTOMERS ROW | CUSTOMER_NAME VARCHAR |
|---|---|---|
| 101 | {customer_id: 101, name: Alice} | Alice |
| 102 | {customer_id: 102, name: Bob} | Bob |
| 103 | {customer_id: 103, name: Charly} | Charly |
| 104 | {customer_id: 104, name: Daniel} | Daniel |
| 105 | {customer_id: 105, name: Elody} | Elody |
| 106 | {customer_id: 106, name: Francis} | Francis |
External data with SQL
When you need to transform or filter data before mapping, use SQL() instead of TABLE(). This lets you write a SQL subquery as the source:
WITH
CUSTOMER_ID := INPUT(BIGINT),
DIM_CUSTOMERS := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
name VARCHAR
FROM SQL(SELECT customer_id, name FROM ext.dim_customers)
)
SELECT
CUSTOMER_ID,
DIM_CUSTOMERS,
CUSTOMER_NAME := DIM_CUSTOMERS[name]
FOR
CUSTOMER_ID := BIND_VALUES(ARRAY[101, 102, 103, 104, 105, 106]);| CUSTOMER_ID BIGINT | DIM_CUSTOMERS ROW | CUSTOMER_NAME VARCHAR |
|---|---|---|
| 101 | {customer_id: 101, name: Alice} | Alice |
| 102 | {customer_id: 102, name: Bob} | Bob |
| 103 | {customer_id: 103, name: Charly} | Charly |
| 104 | {customer_id: 104, name: Daniel} | Daniel |
| 105 | {customer_id: 105, name: Elody} | Elody |
| 106 | {customer_id: 106, name: Francis} | Francis |
External data with VIEW (realtime serving)
Views work the same way as TABLE() but are used in realtime serving contexts, where the data source is a live database view rather than a batch table:
WITH
CUSTOMER_ID := INPUT(BIGINT),
DIM_CUSTOMERS := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
name VARCHAR
FROM VIEW(ext.v_dim_customers)
)
SELECT
CUSTOMER_ID,
DIM_CUSTOMERS,
CUSTOMER_NAME := DIM_CUSTOMERS[name]
FOR
CUSTOMER_ID := BIND_VALUES(ARRAY[101, 102, 103, 104, 105, 106]); For more on realtime connectors (Redis, JDBC, HTTP), see Realtime serving context .