Enrich rows with EXTEND()
EXTEND() keeps every field in a row or array of rows and adds fields computed from another entity grain. Use VIA for key-based enrichment and ON for range, as-of, or other predicate-based enrichment.
Syntax
-- Key relationship EXTEND( <base> WITH <feature> [AS <field>], ... [VIA <base_field>, ... [BIND TO <target>, ...]] ) -- General predicate EXTEND( <base> WITH <aggregate_expression> [AS <field>], ... ON <predicate_using_BASE_fields> )
The base can be one ROW or an ARRAY(ROW). WITH names the values appended to each base row; use AS to control their output field names.
Follow keys with VIA
VIA names fields already present in the base. BIND TO maps them to target inputs when entity types cannot infer the relationship. This self-contained example enriches one order row with two store fields.
WITH
-- ** Setup the relational model **
orders := ENTITY(),
order_id := INPUT(BIGINT#orders),
stores := ENTITY(),
store_id := INPUT(BIGINT#stores),
-- ** Declare external data sources **
tables.orders := INLINE_COLUMNS(
order_id BIGINT#orders BIND TO order_id,
order_store_id BIGINT#stores
FROM CSV(
order_id,order_store_id
10,20
11,21
12,22
13,21
14,22
15,21
)
),
tables.stores := INLINE_COLUMNS(
store_id BIGINT#stores BIND TO store_id,
store_category VARCHAR,
store_city VARCHAR
FROM CSV(
store_id,store_category,store_city
20,PIZZA,BCN
21,SUSHI,MAD
22,BURGER,VAL
)
),
-- ** Define new features **
order_store_id := tables.orders[order_store_id],
order_store_id_details := EXTEND(
ROW(order_store_id AS order_store_id)
WITH tables.stores[store_category] AS store_category,
tables.stores[store_city] AS store_city
VIA order_store_id BIND TO store_id
)
SELECT
order_id,
order_store_id,
order_store_id_details
FOR
order_id := BIND_VALUES(ARRAY(10, 12, 15))
;| ORDER_ID BIGINT | ORDER_STORE_ID BIGINT | ORDER_STORE_ID_DETAILS ROW |
|---|---|---|
| 10 | 20 | {order_store_id: 20, store_category: PIZZA, store_city: BCN} |
| 12 | 22 | {order_store_id: 22, store_category: BURGER, store_city: VAL} |
| 15 | 21 | {order_store_id: 21, store_category: SUSHI, store_city: MAD} |
The same form applies to arrays. Start from an existing array of rows, or use ZIP() to turn a scalar key array into named rows before extending it.
WITH
-- ** Setup the relational model **
customers := ENTITY(),
orders := ENTITY(),
customer_id := INPUT(BIGINT#customers), -- We just need our primary key CUSTOMER_ID as inputs
order_id := INPUT(BIGINT#orders),
-- ** Simulating a customer table. Notice the list of order_ids. **
customer_source := INLINE_COLUMNS(
customer_id BIGINT#customers BIND TO customer_id,
order_ids ARRAY(BIGINT#orders)
FROM CSV(
customer_id,order_ids
1,"[10, 13]"
2,"[11, 14, 15]"
3,"[12]"
)
),
order_ids := customer_source[order_ids],
-- ** Simulating an order table. Notice there is no customer_id foreign key. **
order_source := INLINE_COLUMNS(
order_id BIGINT#orders BIND TO order_id,
price DOUBLE
FROM CSV(
order_id,price
10,12.51
11,13.20
12,15.06
13,25.70
14,30.26
15,10.00
)
),
-- ** We need to add the price to each entry of the ORDER_IDS feature **
order_ids_details := EXTEND(
ZIP(order_ids AS order_id)
WITH order_source[price] AS order_price
VIA order_id BIND TO order_id
)
SELECT
customer_id,
order_ids_details,
revenue_per_customer := ARRAY_SUM(order_ids_details[order_price])
FOR
customer_id := BIND_VALUES(ARRAY(1, 2, 3))
;| CUSTOMER_ID BIGINT | ORDER_IDS_DETAILS VARBINARY | REVENUE_PER_CUSTOMER DECIMAL |
|---|---|---|
| 1 | [{order_id: 10, order_price: 12.51}, {order_id: 13, order_price: 25.70}] | 38.21 |
| 2 | [{order_id: 11, order_price: 13.20}, {order_id: 14, order_price: 30.26}, {order_id: 15, order_price: 10.00}] | 53.46 |
| 3 | [{order_id: 12, order_price: 15.06}] | 15.06 |
EXTEND() can add a feature that is itself an aggregation at another grain, so key-based enrichment composes naturally across several entity hops.
Match ranges and inequalities with ON
Use ON when no single equi key describes the relationship. Inside the predicate, refer to the current base row as BASE[field] and refer to the nested source through its normal features. Bare base field names are rejected so the two sides remain unambiguous.
WITH
product_rows := ZIP(
ARRAY(1::BIGINT, 2::BIGINT, 3::BIGINT) AS product_id,
ARRAY(
52.20::DECIMAL(10,2),
302.00::DECIMAL(10,2),
1200.05::DECIMAL(10,2)
) AS price
),
bands := ENTITY(),
band_id := INPUT(BIGINT#bands),
band_data := INLINE_COLUMNS(
band_id BIGINT#bands BIND TO band_id,
name VARCHAR,
price_min DECIMAL(10,2),
price_max DECIMAL(10,2)
FROM CSV(
band_id,name,price_min,price_max
100,low,0.,100.
101,mid,100.,1000.
102,high,1000.,100000.
)
),
enriched := EXTEND(
product_rows
WITH MAX_BY(BAND_DATA[NAME], BAND_DATA[PRICE_MAX]) AS price_band
ON BAND_DATA[PRICE_MIN] <= BASE[PRICE]
AND BASE[PRICE] < BAND_DATA[PRICE_MAX]
)
SELECT
enriched
FOR
NESTED band_id := BIND_VALUES(ARRAY(100, 101, 102))
;| ENRICHED VARBINARY |
|---|
| [{product_id: 1, price: 52.20, price_band: low}, {product_id: 2, price: 302.00, price_band: mid}, {product_id: 3, price: 1200.05, price_band: high}] |
The complete match—including any equality conjuncts—belongs in ON. Do not combine ON with VIA or BIND TO.
Collapse matches in WITH
An ON predicate may match several inner rows. Every added WITH value must therefore be an aggregate such as MAX_BY(), COUNT(), or ARRAY_AGG(). EXTEND() preserves each base row and returns the aggregate's normal empty-match result: NULL for MAX_BY(), 0 for COUNT(*).
As-of enrichment follows the same pattern: match events at or before BASE[ref_ts], then use MAX_BY(value, ts) to select the latest event.
WITH
accounts := ENTITY(),
account_rows := ZIP(
ARRAY(1::BIGINT, 2::BIGINT) AS account_id,
ARRAY(TIMESTAMP '2024-06-15 12:00:00', TIMESTAMP '2024-06-20 12:00:00') AS ref_ts
),
events := ENTITY(),
event_id := INPUT(BIGINT#events),
event_data := INLINE_COLUMNS(
event_id BIGINT#events BIND TO event_id,
account_id BIGINT#accounts,
event_ts TIMESTAMP,
status VARCHAR
FROM CSV(
event_id,account_id,event_ts,status
1,1,2024-06-01 10:00:00,new
2,1,2024-06-10 10:00:00,active
3,1,2024-06-20 10:00:00,paused
4,2,2024-06-05 10:00:00,new
5,2,2024-06-25 10:00:00,active
)
),
enriched := EXTEND(
account_rows
WITH MAX_BY(EVENT_DATA[STATUS], EVENT_DATA[EVENT_TS]) AS last_status
ON EVENT_DATA[ACCOUNT_ID] = BASE[ACCOUNT_ID]
AND EVENT_DATA[EVENT_TS] <= BASE[REF_TS]
)
SELECT
enriched
FOR
NESTED event_id := BIND_VALUES(ARRAY(1, 2, 3, 4, 5))
;| ENRICHED VARBINARY |
|---|
| [{account_id: 1, ref_ts: 2024-06-15T12:00:00, last_status: active}, {account_id: 2, ref_ts: 2024-06-20T12:00:00, last_status: new}] |
Bind inputs needed only by the source being matched as NESTED; they define the inner relation without multiplying the outer query rows.
When to use another operation
- Use
RELATED()when only one scalar or row is needed at the outer entity grain. - Use
EXTEND()when base rows should survive and gain fields from another grain. - Use
ARRAY_MERGE()when both relations are already arrays and all matches should fan out for later processing.