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.

ContractSyntaxQuestion it answers
DatatypeDECIMAL(18,2)What shape of value?
EntityBIGINT#ORDERSWhat object does this key identify?
UnitBIGINT@CURRENCY.USD_CENTWhat does this number measure?
MultiplicityDETERMINED BY (order_id), PERHow often does the value occur, and how should an aggregate count it?
AdditivityNOT 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:

FeatureQL
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)
;
Result
ERROR UE/RELATED-ENTITY-MISMATCH RELATED() VIA ... requires the same entity family on both sides when entity annotations are present; field 'ORD[order_id]' has entity family 'ORDERS' on the base side but entity family 'CUSTOMERS' on the bound side when defining feature WRONG.

RELATED does not infer joins. The foreign key is always written in VIA, so there is no ambiguity between several join paths.

DirectionFormReads 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-equiRELATED(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:

FeatureQL
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)
;
Result
ERROR UE/ANNOTATIONS-NOT-ALLOWED Function ADD() does not support entity annotations in its parameter 'expr1' when defining feature LAUNDERED. Drop the entity with KEY_TO_SCALAR().

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:

FeatureQL
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)
;
Result
ERROR UE/UNIT-LABEL-MISMATCH Unit labels must match for ADD(); got USD and EUR. Annotate or convert one operand so both share the same unit when defining feature TOTAL.

Units compose under multiplication and division, so an exchange rate is a typed value:

FeatureQL
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)
;
Result
PRICE_EUR DECIMAL(18,2)@CURRENCY.EURPRICE_USD DECIMAL(36,6)@CURRENCY.USDUSD_TYPE VARCHAR
100.00110.000000DECIMAL(36,6)@CURRENCY.USD

Applying the rate in the wrong direction is not an error. The resulting type shows it:

FeatureQL
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)
;
Result
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:

FeatureQL
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))
;
Result
ERROR UE/SUMMARIZABILITY-DETERMINANT-PER-REQUIRED Summing function SUM() consumes SHIPPING_FEE at a finer grain, but SHIPPING_FEE is DETERMINED BY (ORDER_ID) and may repeat. Use SUM(SHIPPING_FEE PER ORDER_ID) to contribute each order once.

The repair named in the message compiles and counts each order once:

FeatureQL
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))
;
Result
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:

FeatureQL
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)
;
Result
CUSTOMER_ID BIGINT#CUSTOMERSSEGMENT VARCHAR
1gold
1silver

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:

FeatureQL
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))
;
Result
ERROR UE/SUMMARIZABILITY-NON-ADDITIVE-AXIS Aggregate SUM(SNAP[STOCK]) GROUP BY (SNAP[PRODUCT_ID]) cannot collapse non-additive axis DAY with SUM(). Preserve DAY or select/recompute one snapshot at the target grain, for example MAX_BY(SNAP[STOCK], DAY).

The repair named in the message returns the latest snapshot. The warning on this block is about ties, see Purity and determinism .

FeatureQL
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))
;
Result
PRODUCT_ID BIGINT#PRODUCTSLATEST_STOCK BIGINT
107

UNSAFE SUM bypasses the check at one call. It is a modifier written before the aggregate, not a function:

FeatureQL
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))
;
Result
PRODUCT_ID BIGINT#PRODUCTSTOTAL_STOCK BIGINT
1012

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, ARRAY and GREATEST mix within a family only.
  • An exact literal may mix with an approximate value: 1.5 + 1e0 is DOUBLE. A named exact value may not: RATE := 1.5 followed by RATE + 1e0 is 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 .