Semantic modeling

Semantic modeling lets FeatureQL check business meaning that ordinary SQL types cannot express. Declare meaning where data enters the model, and the compiler carries it through every derived feature.

The five semantic questions

FeatureQL keeps five properties separate:

PropertyQuestionTypical syntax
DatatypeWhat shape of value is this?DECIMAL(18,2)
EntityWhat business object does this key identify?BIGINT#ORDERS
UnitWhat does this number measure?BIGINT@CURRENCY.USD_CENT
MultiplicityHow often does this value occur on the current rows, and how should this aggregate count it?PER ORDER_ID, PER ROW
AdditivityMay this value be combined across the axes being removed?NOT ADDITIVE, NOT ADDITIVE ACROSS (...)

These properties complement one another; none replaces another. A value can have the correct unit but still be repeated across rows, or contribute once per entity but still be unsafe to add across time.

Datatype, entity, unit, and additivity are declared. Multiplicity is different: the compiler works out how often each value occurs from the query itself, and you only write PER to say how an aggregate should count a value that repeats.

The compiler applies one policy to every aggregate. When it can prove the rollup is safe, it stays quiet. When it can prove the rollup is wrong, it rejects the query and names a repair. When it cannot tell, it runs the query and warns.

Declare meaning once

This example combines all five properties:

FeatureQL
DEFINE
    orders := ENTITY(),
    days := ENTITY(),
    items := ENTITY(),
    order_day_key := INPUT(ROW<order_id BIGINT#orders, day_id DATE#days>),
    order_id := INPUT(BIGINT#orders),
    facts := INLINE_COLUMNS(
        order_day_key ROW(order_id BIGINT#orders, day_id DATE#days) BIND TO order_day_key,
        order_total BIGINT@CURRENCY.USD_CENT DETERMINED BY (ORDER_ID),
        daily_stock BIGINT@ITEMS NOT ADDITIVE ACROSS (ORDER_DAY_KEY[DAY_ID])
        FROM JSON([
            {
                "order_day_key": {"order_id": 10, "day_id": "2026-01-01"},
                "order_total": 3000,
                "daily_stock": 10
            },
            {
                "order_day_key": {"order_id": 10, "day_id": "2026-01-02"},
                "order_total": 3000,
                "daily_stock": 12
            },
            {
                "order_day_key": {"order_id": 20, "day_id": "2026-01-03"},
                "order_total": 2000,
                "daily_stock": 5
            }
        ])
    ),
    day_id := order_day_key[day_id],
    order_total := facts[order_total],
    daily_stock := facts[daily_stock]
RETURN
    booked_revenue := SUM(order_total) PER order_id,
    latest_stock := MAX_BY(daily_stock, day_id)
FOR
    (order_day_key, order_id) := BIND_COLUMNS(
        order_day_key,
        order_id
        FROM JSON([
        {"order_day_key": {"order_id": 10, "day_id": "2026-01-01"}, "order_id": 10},
        {"order_day_key": {"order_id": 10, "day_id": "2026-01-02"}, "order_id": 10},
        {"order_day_key": {"order_id": 20, "day_id": "2026-01-03"}, "order_id": 20}
    ])
    )
;
Result
BOOKED_REVENUE BIGINTLATEST_STOCK BIGINT
50005

Notice what each declaration contributes:

  • BIGINT#ORDERS and DATE#DAYS identify business keys.
  • @CURRENCY.USD_CENT and @ITEMS describe what the numbers measure.
  • DETERMINED BY (ORDER_ID) records that an order total may repeat below order grain.
  • PER ORDER_ID contributes each order total once.
  • NOT ADDITIVE ACROSS (order_day_key[day_id]) prevents inventory snapshots from being added across the day field of the composite key.
  • day_id := ORDER_DAY_KEY[day_id] names that same axis, and MAX_BY(daily_stock, day_id) explicitly chooses the latest snapshot.

The generated SQL contains the operations needed for PER and MAX_BY(). Entity, unit, and additivity annotations are compile-time contracts.

Put semantics at the boundary

Declare entity keys with INPUT(). Put business data columns, units, and metric contracts in EXTERNAL_COLUMNS (or INLINE_COLUMNS for examples). Extraction and formulas then inherit what the compiler can prove.

ORDERS := ENTITY(),
ORDER_ID := INPUT(BIGINT#ORDERS),

ORDER_FACTS := EXTERNAL_COLUMNS(
    order_id    BIGINT#ORDERS BIND TO ORDER_ID,
    order_total DECIMAL(18,2)@CURRENCY.USD DETERMINED BY (order_id),
    FROM TABLE(sales.order_facts)
),

Do not repeatedly relabel derived features. If a derived contract cannot be inferred, inspect the formula or add meaning at its true source.

Choose the focused guide

For ordinary datatypes and casts, see Types .