Semantic contracts
A feature's type carries five independent contracts. They are declared where data enters (EXTERNAL_COLUMNS, INLINE_COLUMNS, INPUT) and never on a derived formula. Derived features inherit what the compiler can prove.
| Contract | Syntax | Question it answers |
|---|---|---|
| Datatype | DECIMAL(18,2) | What shape of value? |
| Entity | BIGINT#ORDERS | What object does this key identify? |
| Unit | BIGINT@CURRENCY.USD_CENT | What does this number measure? |
| Multiplicity | DETERMINED BY (order_id), PER | How often does the value occur, and how should an aggregate count it? |
| Additivity | NOT ADDITIVE ACROSS (day) | May it be summed across a removed axis? |
Each contract governs one property and none overrides another: PER does not override NOT ADDITIVE, and a correct unit does not prove a safe rollup. SHOW FEATURES (COLUMNS (NAME, UNIT)) and TYPEOF() report the contracts of a feature without reading its formula.
Entity
BIGINT#CUSTOMERS and BIGINT#ORDERS are both 64-bit integers in the warehouse and different types in FeatureQL. The annotation has no runtime cost and is checked at compile time. A lookup through the wrong key is refused:
DEFINE
customers := ENTITY(),
orders := ENTITY(),
customer_id := INPUT(BIGINT#customers),
order_id := INPUT(BIGINT#orders),
cust := INLINE_COLUMNS(
customer_id BIGINT#customers BIND TO customer_id,
segment VARCHAR
FROM JSON([{"customer_id": 1, "segment": "gold"}])
),
ord := INLINE_COLUMNS(
order_id BIGINT#orders BIND TO order_id,
customer_id BIGINT#customers
FROM JSON([{"order_id": 101, "customer_id": 1}])
),
wrong := RELATED(cust[segment] VIA ord[order_id])
RETURN
order_id,
wrong
FOR
order_id := BIND_VALUE(101)
;RELATED does not infer joins. The foreign key is always written in VIA, so there is no ambiguity between several join paths.
| Direction | Form | Reads as |
|---|---|---|
| Foreign key to primary key (lookup) | RELATED(target VIA source[fk]) | the customer's segment, for this invoice |
| Primary key to foreign key (aggregate) | PK.RELATED(AGG(...) GROUP BY (source[fk])) | the sum of this customer's invoices |
| Non-equi | RELATED(AGG(...) ON pred) | range, as-of |
Annotations survive composition: through RELATED hops, into arrays, into persisted features. Arithmetic on a key is refused, so a key cannot become a plain integer by accident:
DEFINE
customers := ENTITY(),
orders := ENTITY(),
customer_id := INPUT(BIGINT#customers),
order_id := INPUT(BIGINT#orders),
laundered := customer_id + 0,
mixed := COALESCE(laundered, order_id)
RETURN
mixed
FOR
customer_id := BIND_VALUE(1),
order_id := BIND_VALUE(2)
;Dropping the entity is written KEY_TO_SCALAR(). Relabelling is written UNSAFE_CAST. See Types .
Unit
A unit is part of the type (DECIMAL(18,2)@CURRENCY.USD). It is checked by the compiler and erased from the generated SQL. Units add and subtract only when equal:
DEFINE
price_usd := INPUT(DECIMAL(18,2)@CURRENCY.USD),
fee_eur := INPUT(DECIMAL(18,2)@CURRENCY.EUR),
total := price_usd + fee_eur
RETURN
total
FOR
price_usd := BIND_VALUE(100.00),
fee_eur := BIND_VALUE(5.00)
;Units compose under multiplication and division, so an exchange rate is a typed value:
DEFINE
price_eur := INPUT(DECIMAL(18,2)@CURRENCY.EUR),
usd_per_eur := 1.10::DECIMAL(18,4)@(CURRENCY.USD / CURRENCY.EUR),
price_usd := price_eur * usd_per_eur
RETURN
price_eur,
price_usd,
usd_type := TYPEOF(price_usd)
FOR
price_eur := BIND_VALUE(100.00)
;| PRICE_EUR DECIMAL(18,2)@CURRENCY.EUR | PRICE_USD DECIMAL(36,6)@CURRENCY.USD | USD_TYPE VARCHAR |
|---|---|---|
| 100.00 | 110.000000 | DECIMAL(36,6)@CURRENCY.USD |
Applying the rate in the wrong direction is not an error. The resulting type shows it:
DEFINE
price_eur := INPUT(DECIMAL(18,2)@CURRENCY.EUR),
usd_per_eur := 1.10::DECIMAL(18,4)@(CURRENCY.USD / CURRENCY.EUR),
wrong_way := price_eur / usd_per_eur
RETURN
wrong_type := TYPEOF(wrong_way)
FOR
price_eur := BIND_VALUE(100.00)
;| WRONG_TYPE VARCHAR |
|---|
| DOUBLE@(CURRENCY.EUR ^ 2 / CURRENCY.USD) |
Multiplicity
BIND TO declares the lookup key of a source. DETERMINED BY (k) declares that a value repeats on finer rows, for example an order-level fee stored on every order line. A collapsing SUM over such a value must say how to count the repeats:
DEFINE
lines := ENTITY(),
orders := ENTITY(),
line_id := INPUT(BIGINT#lines),
l := INLINE_COLUMNS(
line_id BIGINT#lines BIND TO line_id,
order_id BIGINT#orders,
shipping_fee DECIMAL(10,2) DETERMINED BY (ORDER_ID)
FROM JSON([
{"line_id": 1, "order_id": 10, "shipping_fee": 4.00},
{"line_id": 2, "order_id": 10, "shipping_fee": 4.00},
{"line_id": 3, "order_id": 20, "shipping_fee": 6.00}
])
),
order_id := l[order_id],
shipping_fee := l[shipping_fee]
RETURN
total_shipping := SUM(shipping_fee)
FOR
line_id := BIND_VALUES(ARRAY(1, 2, 3))
;The repair named in the message compiles and counts each order once:
DEFINE
lines := ENTITY(),
orders := ENTITY(),
line_id := INPUT(BIGINT#lines),
l := INLINE_COLUMNS(
line_id BIGINT#lines BIND TO line_id,
order_id BIGINT#orders,
shipping_fee DECIMAL(10,2) DETERMINED BY (ORDER_ID)
FROM JSON([
{"line_id": 1, "order_id": 10, "shipping_fee": 4.00},
{"line_id": 2, "order_id": 10, "shipping_fee": 4.00},
{"line_id": 3, "order_id": 20, "shipping_fee": 6.00}
])
),
order_id := l[order_id],
shipping_fee := l[shipping_fee]
RETURN
total_shipping := SUM(shipping_fee) PER order_id
FOR
line_id := BIND_VALUES(ARRAY(1, 2, 3))
;| TOTAL_SHIPPING DECIMAL(38,2) |
|---|
| 10.00 |
Grain is declared, not verified. The compiler does not read the source, so it cannot check that a key is unique. Two rows with the same key fan out:
DEFINE
customers := ENTITY(),
customer_id := INPUT(BIGINT#customers),
cust := INLINE_COLUMNS(
customer_id BIGINT#customers BIND TO customer_id,
segment VARCHAR
FROM JSON([
{"customer_id": 1, "segment": "gold"},
{"customer_id": 1, "segment": "silver"}
])
)
RETURN
customer_id,
segment := cust[segment]
FOR
customer_id := BIND_VALUE(1)
;| CUSTOMER_ID BIGINT#CUSTOMERS | SEGMENT VARCHAR |
|---|---|
| 1 | gold |
| 1 | silver |
Checking a declaration against real data is a query on your warehouse, run as a test.
Additivity
Numeric sources are additive by default. A source column can be declared NOT ADDITIVE (a rate) or NOT ADDITIVE ACROSS (day) (a snapshot). The declaration is carried to every derived value. A SUM that would collapse the snapshot axis is refused:
DEFINE
snapshots := ENTITY(),
products := ENTITY(),
snap_id := INPUT(BIGINT#snapshots),
product_id := INPUT(BIGINT#products),
snap := INLINE_COLUMNS(
snap_id BIGINT#snapshots BIND TO snap_id,
product_id BIGINT#products,
day DATE,
stock BIGINT NOT ADDITIVE ACROSS (DAY)
FROM JSON([
{"snap_id": 1, "product_id": 10, "day": "2026-01-01", "stock": 5},
{"snap_id": 2, "product_id": 10, "day": "2026-01-02", "stock": 7}
])
),
total_stock := product_id.RELATED(
SUM(snap[stock]) GROUP BY (snap[product_id])
)
RETURN
product_id,
total_stock
FOR
product_id := BIND_VALUE(10),
NESTED snap_id := BIND_VALUES(ARRAY(1, 2))
;The repair named in the message returns the latest snapshot. The warning on this block is about ties, see Purity and determinism .
DEFINE
snapshots := ENTITY(),
products := ENTITY(),
snap_id := INPUT(BIGINT#snapshots),
product_id := INPUT(BIGINT#products),
snap := INLINE_COLUMNS(
snap_id BIGINT#snapshots BIND TO snap_id,
product_id BIGINT#products,
day DATE,
stock BIGINT NOT ADDITIVE ACROSS (DAY)
FROM JSON([
{"snap_id": 1, "product_id": 10, "day": "2026-01-01", "stock": 5},
{"snap_id": 2, "product_id": 10, "day": "2026-01-02", "stock": 7}
])
),
latest_stock := product_id.RELATED(
MAX_BY(snap[stock], snap[day]) GROUP BY (snap[product_id])
)
RETURN
product_id,
latest_stock
FOR
product_id := BIND_VALUE(10),
NESTED snap_id := BIND_VALUES(ARRAY(1, 2))
;| PRODUCT_ID BIGINT#PRODUCTS | LATEST_STOCK BIGINT |
|---|---|
| 10 | 7 |
UNSAFE SUM bypasses the check at one call. It is a modifier written before the aggregate, not a function:
DEFINE
snapshots := ENTITY(),
products := ENTITY(),
snap_id := INPUT(BIGINT#snapshots),
product_id := INPUT(BIGINT#products),
snap := INLINE_COLUMNS(
snap_id BIGINT#snapshots BIND TO snap_id,
product_id BIGINT#products,
day DATE,
stock BIGINT NOT ADDITIVE ACROSS (DAY)
FROM JSON([
{"snap_id": 1, "product_id": 10, "day": "2026-01-01", "stock": 5},
{"snap_id": 2, "product_id": 10, "day": "2026-01-02", "stock": 7}
])
),
total_stock := product_id.RELATED(
UNSAFE SUM(snap[stock]) GROUP BY (snap[product_id])
)
RETURN
product_id,
total_stock
FOR
product_id := BIND_VALUE(10),
NESTED snap_id := BIND_VALUES(ARRAY(1, 2))
;| PRODUCT_ID BIGINT#PRODUCTS | TOTAL_STOCK BIGINT |
|---|---|
| 10 | 12 |
Datatype
There is no implicit widening between computed values, because widening rules differ per backend. Numerics have two categories (exact, approximate) and three families (integer and decimal are exact, floating is approximate).
- Operators such as
+and*mix within a category, so integer and decimal combine. COALESCE,IF,CASE,ARRAYandGREATESTmix within a family only.- An exact literal may mix with an approximate value:
1.5 + 1e0isDOUBLE. A named exact value may not:RATE := 1.5followed byRATE + 1e0is an error.
A cast is written when the category or family changes. See Types .
Limits
The checks follow from the declarations. A wrong DETERMINED BY or a duplicate key at the source gives a wrong result with no error. Each such assumption, and the warehouse check that belongs to it, is Data contracts . See the overview .