FeatureQL error catalog: what users write, what they mean, and what to tell them

This document catalogs common modeling mistakes in FeatureQL. Each entry has four parts:

  1. What the user wrote (the broken query)
  2. What they meant (the intent)
  3. Error code and message (what the engine should return)
  4. Trace / fix hint (actionable guidance to correct the mistake)

Errors are grouped by category. Existing error codes (already in the engine) are marked with [EXISTS]. New proposed codes are marked with [NEW].


1. Type system errors

1.1 Mixing BIGINT and DOUBLE without cast

WITH
    PRICE := INPUT(DOUBLE),
    TAXED := PRICE * 1.25,
SELECT TAXED
FOR PRICE := BIND_VALUE(100e0);
sql

Intent: multiply a DOUBLE by 1.25.

Problem: 1.25 is a DECIMAL(3,2) literal, not DOUBLE. PRICE is DOUBLE. No automatic coercion between DOUBLE and DECIMAL.

Error: [EXISTS] UE/TYPE-MISMATCH

Error UE/TYPE-MISMATCH at line 3, col 18

  TAXED := PRICE * 1.25,
                   ^^^^
  Cannot apply operator '*' to DOUBLE and DECIMAL(3,2).
  FeatureQL does not auto-coerce between DOUBLE and DECIMAL.

Fix: use scientific notation for DOUBLE literals: 1.25e0
     or cast explicitly: CAST(1.25 AS DOUBLE)
null

1.2 BIGINT and DECIMAL mixed in ARRAY constructor

WITH
    VALUES := ARRAY[1.25, 1],
SELECT VALUES;
sql

Intent: build an array with a decimal and an integer.

Problem: 1.25 is DECIMAL(3,2), 1 is BIGINT. Integer promotion to DECIMAL does NOT apply inside ARRAY constructors.

Error: [NEW] UE/ARRAY-MIXED-TYPE-FAMILIES

Error UE/ARRAY-MIXED-TYPE-FAMILIES at line 2, col 16

  VALUES := ARRAY[1.25, 1],
                  ^^^^^^^^
  Array elements mix DECIMAL(3,2) and BIGINT.
  DECIMAL widening works (e.g. ARRAY[1.25, 1.1] is fine),
  but BIGINT-to-DECIMAL promotion does not apply inside ARRAY or ROW constructors.

Fix: cast the integer to DECIMAL: ARRAY[1.25, 1.00]
     or use DOUBLE throughout: ARRAY[1.25e0, 1e0]
null

1.3 BETWEEN with mismatched types

WITH
    ID := INPUT(BIGINT),
    AMOUNT := INPUT(DOUBLE),
    IN_RANGE := AMOUNT BETWEEN 10 AND 100,
SELECT IN_RANGE
FOR (ID, AMOUNT) := BIND_VALUES(ARRAY[ROW(1, 50e0)]);
sql

Intent: check if a DOUBLE value is between two bounds.

Problem: 10 and 100 are BIGINT, AMOUNT is DOUBLE. BETWEEN requires all three operands to be the same type.

Error: [NEW] UE/BETWEEN-TYPE-MISMATCH

Error UE/BETWEEN-TYPE-MISMATCH at line 4, col 17

  IN_RANGE := AMOUNT BETWEEN 10 AND 100,
              ^^^^^^^^^^^^^^^^^^^^^^^^^^
  BETWEEN operands must all be the same type.
  Got: DOUBLE BETWEEN BIGINT AND BIGINT.

Fix: use DOUBLE literals: AMOUNT BETWEEN 10e0 AND 100e0
null

1.4 Untyped NULL in CASE expression

WITH
    ID := INPUT(BIGINT),
    STATUS := INPUT(VARCHAR),
    LABEL := CASE WHEN STATUS = 'active' THEN DATE '2024-01-01' ELSE NULL END,
SELECT LABEL
FOR (ID, STATUS) := BIND_VALUES(ARRAY[ROW(1, 'active')]);
sql

Intent: return a date or null.

Problem: bare NULL has no type. FeatureQL requires typed nulls so it can verify branch types match.

Error: [EXISTS] UE/CASE_WHEN-MIXED-TYPES

Error UE/CASE_WHEN-MIXED-TYPES at line 4, col 5

  LABEL := CASE WHEN STATUS = 'active' THEN DATE '2024-01-01' ELSE NULL END,
                                                                    ^^^^
  THEN branch returns DATE, but ELSE branch returns untyped NULL.
  Every branch in CASE/IF must produce the same type, including NULLs.

Fix: use a typed null: ELSE NULL(DATE) END
null

1.5 Bare IS NULL without type annotation

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    FIRST_ORDER := CUSTOMER_ID.RELATED(
        MIN(ORDER_DATE) GROUP BY FCT_ORDERS[customer_id]
    ),
SELECT CUSTOMER_ID
WHERE FIRST_ORDER IS NOT NULL
FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);
sql

Intent: filter customers who have at least one order.

Problem: FIRST_ORDER might resolve to EMPTY type when no rows match the RELATED. Bare IS NOT NULL cannot determine the type to compare against.

Error: [EXISTS] UE/COMPARISON-TYPE-EMPTY

Error UE/COMPARISON-TYPE-EMPTY at line 7

  WHERE FIRST_ORDER IS NOT NULL
                    ^^^^^^^^^^^
  IS NULL / IS NOT NULL requires a type annotation when the expression
  can resolve to EMPTY (common with RELATED results that match zero rows).

Fix: WHERE FIRST_ORDER IS NOT NULL::DATE
null

1.6 DOUBLE comparison without precision specifier

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    DIM := EXTERNAL_COLUMNS(
        customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
        score DOUBLE
        FROM TABLE(schema.customers)
    ),
    HIGH_SCORE := DIM[score] > 0.5,
SELECT CUSTOMER_ID
WHERE HIGH_SCORE
FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);
sql

Intent: filter customers with score above 0.5.

Problem: 0.5 is DECIMAL, DIM[score] is DOUBLE. Even if that were fixed, comparing DOUBLEs with > can be lossy. FeatureQL expects precision-aware operators like >.2 for DOUBLE comparisons.

Error: [EXISTS] UE/COMPARISON-DIGITS-EMPTY

Error UE/COMPARISON-DIGITS-EMPTY at line 8, col 26

  HIGH_SCORE := DIM[score] > 0.5,
                           ^^^^^
  DOUBLE comparisons require a precision specifier: >.N
  where N is the number of decimal digits to consider.

Fix: DIM[score] >.2 0.5e0
     (compares to 2 decimal places, and 0.5e0 is DOUBLE)
null

2. Binding and INPUT errors

2.1 Missing FOR when SELECT depends on INPUT

WITH
    ID := INPUT(BIGINT),
    DOUBLED := ID * 2,
SELECT ID, DOUBLED;
sql

Intent: compute doubled values for some IDs.

Problem: ID is an INPUT() but there is no FOR clause to bind it. This is the single most common mistake.

Error: [NEW] UE/MISSING-BINDING

Error UE/MISSING-BINDING

  SELECT depends on INPUT feature 'ID' (declared at line 2),
  but no FOR clause provides a binding for it.

  Every INPUT() in the dependency graph of SELECT must be bound in FOR.

Fix: add a FOR clause:
     FOR ID := BIND_VALUES(ARRAY[1, 2, 3]);
null

2.2 Forgetting to bind a transitive INPUT

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    ORDER_ID := INPUT(BIGINT#ORDERS),
    ORDER_CUSTOMER_ID := FCT_ORDERS[customer_id],
    CUSTOMER_NAME := DIM_CUSTOMERS[name],
    ORDER_CUSTOMER_NAME := RELATED(CUSTOMER_NAME VIA FCT_ORDERS[customer_id]),
SELECT ORDER_ID, ORDER_CUSTOMER_NAME
FROM fm.shop
FOR ORDER_ID := BIND_VALUES(ARRAY[10, 20]);
sql

Intent: get customer name for each order.

Problem: ORDER_CUSTOMER_NAME uses RELATED which resolves through CUSTOMER_NAME, which depends on CUSTOMER_ID (an INPUT). Only ORDER_ID is bound. CUSTOMER_ID is missing from FOR.

Error: [NEW] UE/UNBOUND-TRANSITIVE-INPUT

Error UE/UNBOUND-TRANSITIVE-INPUT

  Feature 'ORDER_CUSTOMER_NAME' (line 6) depends on INPUT 'CUSTOMER_ID'
  through the chain:
    ORDER_CUSTOMER_NAME
      -> CUSTOMER_NAME (via RELATED)
        -> DIM_CUSTOMERS
          -> CUSTOMER_ID (INPUT, not bound)

  Only ORDER_ID is bound in FOR.

Fix: bind CUSTOMER_ID as well. If you want a cross product:
     FOR CROSS
         ORDER_ID := BIND_VALUES(ARRAY[10, 20]),
         CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);

     If keys should be paired, use a single BIND_VALUES with ROWs.
null

2.3 Multiple separate BIND_VALUES without FOR CROSS

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    ORDER_ID := INPUT(BIGINT#ORDERS),
SELECT CUSTOMER_ID, ORDER_ID
FOR
    CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]),
    ORDER_ID := BIND_VALUES(ARRAY[10, 20]);
sql

Intent: get all combinations of customers and orders.

Problem: separate BIND_VALUES in plain FOR is ambiguous. FeatureQL requires explicit FOR CROSS when you intend a Cartesian product.

Error: [NEW] UE/CROSS-PRODUCT-REQUIRES-FOR-CROSS

Error UE/CROSS-PRODUCT-REQUIRES-FOR-CROSS at line 5

  FOR
      CUSTOMER_ID := BIND_VALUES(...),
      ORDER_ID := BIND_VALUES(...);

  Multiple independent BIND_VALUES produce a Cartesian product (2 x 2 = 4 rows).
  Plain FOR does not allow this; use FOR CROSS to opt in explicitly.

Fix: if you want all combinations:
     FOR CROSS
         CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]),
         ORDER_ID := BIND_VALUES(ARRAY[10, 20]);

     If keys should be paired (1 with 10, 2 with 20):
     FOR (CUSTOMER_ID, ORDER_ID) := BIND_VALUES(ARRAY[ROW(1, 10), ROW(2, 20)]);
null

3. Entity and relationship errors

3.1 Missing #ENTITY annotation on key column in EXTERNAL_COLUMNS

DIM_CUSTOMERS := EXTERNAL_COLUMNS(
    customer_id BIGINT BIND TO CUSTOMER_ID,
    name VARCHAR
    FROM TABLE(schema.dim_customers)
),
sql

Intent: map the customers table.

Problem: customer_id is declared as plain BIGINT, but CUSTOMER_ID is INPUT(BIGINT#CUSTOMERS). Missing #CUSTOMERS on the key column breaks entity resolution for RELATED/EXTEND.

Error: [NEW] UE/BIND-TO-ENTITY-MISMATCH

Error UE/BIND-TO-ENTITY-MISMATCH at line 2, col 5

  customer_id BIGINT BIND TO CUSTOMER_ID,
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Key column 'customer_id' is typed as BIGINT,
  but CUSTOMER_ID is INPUT(BIGINT#CUSTOMERS).

  The #ENTITY annotation must match between the key column and its INPUT.

Fix: customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
null

3.2 Missing ENTITY() declaration

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    DIM := EXTERNAL_COLUMNS(
        customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
        name VARCHAR
        FROM TABLE(schema.customers)
    ),
SELECT CUSTOMER_ID, DIM[name]
FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);
sql

Intent: query customer data.

Problem: there is no CUSTOMERS := ENTITY() declaration. The #CUSTOMERS annotation references an entity that does not exist.

Error: [NEW] UE/ENTITY-NOT-DECLARED

Error UE/ENTITY-NOT-DECLARED at line 2

  CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
                              ^^^^^^^^^^
  Entity 'CUSTOMERS' is referenced but never declared.

Fix: add the entity declaration before the INPUT:
     CUSTOMERS := ENTITY(),
     CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
null

3.3 Two BIND TO in one EXTERNAL_COLUMNS

DIM_ORDER_CUSTOMER := EXTERNAL_COLUMNS(
    order_id BIGINT#ORDERS BIND TO ORDER_ID,
    customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
    amount DOUBLE
    FROM TABLE(schema.orders)
),
sql

Intent: map a table keyed by both order and customer.

Problem: EXTERNAL_COLUMNS supports exactly one BIND TO per mapping. One key per entity.

Error: [NEW] UE/EXTERNAL-COLUMNS-MULTIPLE-BIND-TO

Error UE/EXTERNAL-COLUMNS-MULTIPLE-BIND-TO at line 1

  DIM_ORDER_CUSTOMER := EXTERNAL_COLUMNS(
      order_id BIGINT#ORDERS BIND TO ORDER_ID,
      customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
      ...
  )

  EXTERNAL_COLUMNS allows exactly one BIND TO column (one key per mapping).

Fix: decide which entity this mapping belongs to. If orders:
     order_id BIGINT#ORDERS BIND TO ORDER_ID,
     customer_id BIGINT#CUSTOMERS,       -- FK, not a key

     If you need data keyed by both entities, create two separate mappings.
null

3.4 Using RELATED with wrong VIA direction

-- Wants: "for each order, get the customer name"
-- FCT_ORDERS has customer_id as FK, DIM_CUSTOMERS has customer_id as PK
ORDER_CUSTOMER_NAME := RELATED(CUSTOMER_NAME VIA DIM_CUSTOMERS[customer_id]),
sql

Intent: look up customer name from an order.

Problem: VIA DIM_CUSTOMERS[customer_id] points at the PK side. For FK-to-PK lookups, the VIA must point at the FK column on the fact table: VIA FCT_ORDERS[customer_id].

Error: [NEW] UE/RELATED-VIA-WRONG-DIRECTION

Error UE/RELATED-VIA-WRONG-DIRECTION at line 1

  RELATED(CUSTOMER_NAME VIA DIM_CUSTOMERS[customer_id])
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  DIM_CUSTOMERS[customer_id] is the primary key of CUSTOMERS.
  For FK->PK lookups, VIA must point at the foreign key column.

  Resolution path attempted:
    ORDER grain -> DIM_CUSTOMERS[customer_id] (PK) -> CUSTOMER_NAME
    This is backwards: there is no FK from customers to orders here.

Fix: RELATED(CUSTOMER_NAME VIA FCT_ORDERS[customer_id])
     (FCT_ORDERS[customer_id] is the FK that points to CUSTOMERS)
null

4. TRANSFORM scope errors

4.1 Referencing a feature name inside TRANSFORM instead of a field name

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    ORDER_AMOUNT := FCT_ORDERS[amount],
    ENRICHED := EXTEND(
        ZIP(CUSTOMER_ORDER_IDS AS order_id)
        WITH ORDER_AMOUNT
        VIA order_id BIND TO ORDER_ID
    ),
    TOTAL := ENRICHED.TRANSFORM(SELECT SUM(ORDER_AMOUNT)).UNWRAP_ONE(),
SELECT CUSTOMER_ID, TOTAL
FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);
sql

Intent: sum the order amounts for each customer.

Problem: inside .TRANSFORM(SELECT ...), only row field names exist. ORDER_AMOUNT is a feature name. After EXTEND, the field is called order_amount (lowercase of the feature name).

Error: [NEW] UE/TRANSFORM-UNKNOWN-FIELD

Error UE/TRANSFORM-UNKNOWN-FIELD at line 9

  TOTAL := ENRICHED.TRANSFORM(SELECT SUM(ORDER_AMOUNT)).UNWRAP_ONE(),
                                         ^^^^^^^^^^^^
  'ORDER_AMOUNT' is not a field in the array. TRANSFORM only sees row fields,
  not outer feature names.

  Available fields in ENRICHED:
    order_id (BIGINT#ORDERS), order_amount (DOUBLE)

  After EXTEND, feature names become lowercase field names.

Fix: ENRICHED.TRANSFORM(SELECT SUM(order_amount))
null

4.2 Referencing an outer scalar inside TRANSFORM without CARRY

WITH
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    THRESHOLD := 100e0,
    ENRICHED := EXTEND(
        ZIP(CUSTOMER_ORDER_IDS AS order_id)
        WITH ORDER_AMOUNT
        VIA order_id BIND TO ORDER_ID
    ),
    BIG_ORDERS := ENRICHED.TRANSFORM(SELECT * WHERE order_amount > THRESHOLD),
SELECT CUSTOMER_ID, BIG_ORDERS
FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);
sql

Intent: filter orders above a threshold defined outside the array.

Problem: THRESHOLD is an outer feature. TRANSFORM cannot see it. You need to put it on each row first with CARRY.

Error: [NEW] UE/TRANSFORM-OUTER-FEATURE-NOT-IN-SCOPE

Error UE/TRANSFORM-OUTER-FEATURE-NOT-IN-SCOPE at line 9

  BIG_ORDERS := ENRICHED.TRANSFORM(SELECT * WHERE order_amount > THRESHOLD),
                                                                 ^^^^^^^^^
  'THRESHOLD' is a top-level feature, not a field in the array.
  TRANSFORM only sees row fields.

  Did you mean to inject this value onto each row?

Fix: use CARRY to make it a row field:
     BIG_ORDERS := ENRICHED.CARRY(THRESHOLD)
         .TRANSFORM(SELECT * WHERE order_amount >.2 threshold),
null

4.3 IMPORT with impure feature

WITH
    PRICE_DATA := EXTERNAL_COLUMNS(
        id BIGINT#PRODUCTS BIND TO PRODUCT_ID,
        base_price DOUBLE
        FROM TABLE(schema.products)
    ),
    PRICE_PLUS_TAX := PRICE_DATA[base_price] * 1.2e0,
    RESULT := TRANSFORM(
        order_items
        USING (SELECT SUM(price_plus_tax) AS total)
        IMPORT PRICE_PLUS_TAX
    ),
sql

Intent: use an outer formula inside TRANSFORM's USING.

Problem: PRICE_PLUS_TAX depends on PRICE_DATA which uses FROM TABLE(...). That makes it impure. IMPORT requires pure features.

Error: [EXISTS] UE/TRANSFORM-IMPORT-FEATURE-NOT-PURE

Error UE/TRANSFORM-IMPORT-FEATURE-NOT-PURE at line 9

  IMPORT PRICE_PLUS_TAX
         ^^^^^^^^^^^^^^
  PRICE_PLUS_TAX (or a dependency in its graph) is impure:
    PRICE_PLUS_TAX -> PRICE_DATA (EXTERNAL_COLUMNS ... FROM TABLE)

  IMPORT requires all imported features and their dependencies to be pure
  (no TABLE, SQL, or VIEW sources).

Fix: use EXTEND to materialize the value as a row field first:
     ENRICHED := EXTEND(order_items WITH PRICE_PLUS_TAX AS price_plus_tax
                        VIA product_id BIND TO PRODUCT_ID),
     RESULT := ENRICHED.TRANSFORM(SELECT SUM(price_plus_tax)),
null

5. Data source and CSV errors

5.1 Single-line CSV in INLINE_COLUMNS

DIM := INLINE_COLUMNS(
    id BIGINT BIND TO ID, name VARCHAR
    FROM CSV(id,name,1,Alice,2,Bob)
),
sql

Intent: mock data with CSV.

Problem: the CSV is on a single line. The parser expects header + one row per line.

Error: [EXISTS] UE/EXTERNAL-COLUMNS-CSV-NO-DATA

Error UE/EXTERNAL-COLUMNS-CSV-NO-DATA at line 3

  FROM CSV(id,name,1,Alice,2,Bob)
       ^^^^^^^^^^^^^^^^^^^^^^^^^^
  CSV block has a header line but no data rows.
  The parser expects real line breaks: header on one line,
  then one data row per line.

Fix: use multiline CSV:
     FROM CSV(
         id,name
         1,Alice
         2,Bob
     )

     Or use JSON to avoid newline issues:
     FROM JSON([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}])
null

5.2 Missing BIND TO in EXTERNAL_COLUMNS

DIM_CUSTOMERS := EXTERNAL_COLUMNS(
    customer_id BIGINT#CUSTOMERS,
    name VARCHAR
    FROM TABLE(schema.dim_customers)
),
sql

Intent: map the customers table.

Problem: no column has BIND TO. The engine cannot look up rows by key.

Error: [NEW] UE/EXTERNAL-COLUMNS-NO-BIND-TO

Error UE/EXTERNAL-COLUMNS-NO-BIND-TO at line 1

  DIM_CUSTOMERS := EXTERNAL_COLUMNS(
      customer_id BIGINT#CUSTOMERS,
      name VARCHAR
      FROM TABLE(schema.dim_customers)
  )

  No column has BIND TO. Every EXTERNAL_COLUMNS must have exactly
  one key column with BIND TO pointing at an INPUT() feature.

Fix: customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
null

5.3 Using BOUND TO instead of BIND TO

DIM_CUSTOMERS := EXTERNAL_COLUMNS(
    customer_id BIGINT#CUSTOMERS BOUND TO CUSTOMER_ID,
    name VARCHAR
    FROM TABLE(schema.dim_customers)
),
sql

Intent: correct mapping with wrong keyword.

Problem: the keyword is BIND TO (two words), not BOUND TO.

Error: [EXISTS] UE/SYNTAX-ERROR

Error UE/SYNTAX-ERROR at line 2, col 38

  customer_id BIGINT#CUSTOMERS BOUND TO CUSTOMER_ID,
                               ^^^^^
  Unexpected token 'BOUND'. Did you mean BIND TO?

Fix: customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
null

6. RELATED and aggregation errors

6.1 CASE WHEN inside RELATED aggregation instead of FILTER

FIRST_FEB_DATE := VISITOR_ID.RELATED(
    MIN(CASE WHEN IS_FEB THEN FCT[session_date] ELSE NULL(DATE) END)
    GROUP BY FCT[visitor_id]
),
sql

Intent: get the earliest session date in February per visitor.

Problem: CASE WHEN / IF expressions inside RELATED aggregation break binding resolution. Use FILTER (WHERE ...) instead.

Error: [EXISTS] UE/EXTEND-UNRESOLVABLE-FEATURES

Error UE/EXTEND-UNRESOLVABLE-FEATURES

  Cannot resolve features inside RELATED aggregation.
  The CASE WHEN expression wrapping FCT[session_date] prevents
  the engine from tracing the binding path.

  FeatureQL's native pattern for conditional aggregation is
  FILTER (WHERE ...), not CASE WHEN inside the aggregate.

Fix:
  IS_FEB := <your boolean feature>,
  FIRST_FEB_DATE := VISITOR_ID.RELATED(
      MIN(FCT[session_date]) FILTER (WHERE IS_FEB)
      GROUP BY FCT[visitor_id]
  ),

  The boolean must be a named feature, not an inline expression.
null

6.2 Bare field name in RELATED instead of mapping reference

TOTAL_REVENUE := CUSTOMER_ID.RELATED(
    SUM(unit_price * quantity)
    GROUP BY FCT_ITEMS[customer_id]
),
sql

Intent: sum revenue from line items per customer.

Problem: unit_price and quantity are bare column names. Inside RELATED, you must reference fields through the mapping feature: FCT_ITEMS[unit_price].

Error: [EXISTS] UE/FEATURE-NOT-FOUND

Error UE/FEATURE-NOT-FOUND at line 2

  SUM(unit_price * quantity)
      ^^^^^^^^^^
  'unit_price' is not a known feature.

  Inside RELATED, reference fields through their mapping:
  FCT_ITEMS[unit_price], not bare unit_price.

Fix: SUM(FCT_ITEMS[unit_price] * FCT_ITEMS[quantity])
null

6.3 GROUP BY on a literal value

GLOBAL_TOTAL := SUM(ORDER_AMOUNT) GROUP BY 1,
sql

Intent: compute a single global sum across all rows.

Problem: FeatureQL rejects GROUP BY on literals. There is no "group everything into one bucket" syntax with GROUP BY.

Error: [EXISTS] UE/AGGREGATE-GROUP-BY-NOT-IDENTIFIER

Error UE/AGGREGATE-GROUP-BY-NOT-IDENTIFIER at line 1

  SUM(ORDER_AMOUNT) GROUP BY 1
                              ^
  GROUP BY requires a feature identifier, not a literal.
  FeatureQL has no literal GROUP BY keys (CONST values won't work either).

Fix: for a global aggregate across all rows, use a window function:
     GLOBAL_TOTAL := SUM(ORDER_AMOUNT) OVER ()::DECIMAL
     (add LIMIT 1 to deduplicate if needed)
null

6.4 Mixing window functions with GROUP BY in the same query

SELECT
    CATEGORY,
    TOTAL := SUM(AMOUNT) GROUP BY CATEGORY,
    PREV := LAG(AMOUNT, 1) OVER (ORDER BY ID ASC),
FOR ID := BIND_VALUES(SEQUENCE(1, 10));
sql

Intent: both aggregate by category and compute lag.

Problem: window functions and GROUP BY cannot coexist in a single query when non-aggregated features remain in scope.

Error: [EXISTS] UE/AGGREGATE-WITH-REPORT

Error UE/AGGREGATE-WITH-REPORT

  Cannot mix window functions (LAG at line 4) with GROUP BY aggregation
  (SUM ... GROUP BY at line 3) in the same query when non-aggregated
  features are in scope.

Fix: split into separate queries or restructure:
     1. Do the GROUP BY aggregation in one query/feature layer
     2. Apply window functions in a separate query that reads from step 1
null

7. Persistence and namespace errors

7.1 Referencing persisted features without FROM

-- Step 1 (already done):
-- CREATE OR REPLACE FEATURES IN fm.shop AS SELECT ...

-- Step 2 (broken):
CREATE OR REPLACE FEATURES IN fm.shop AS
SELECT
    ORDER_COUNT := CUSTOMER_ID.RELATED(COUNT(1) GROUP BY FCT_ORDERS[customer_id]);
sql

Intent: add a derived feature that references previously persisted features.

Problem: CUSTOMER_ID and FCT_ORDERS were persisted in step 1, but step 2 does not include FROM fm.shop. Persisted features are not automatically in scope.

Error: [EXISTS] UE/FEATURE-NOT-FOUND

Error UE/FEATURE-NOT-FOUND at line 3

  CUSTOMER_ID.RELATED(...)
  ^^^^^^^^^^^
  'CUSTOMER_ID' is not defined in this query.

  Features persisted with CREATE FEATURES are not automatically in scope.
  You must import them with FROM.

Fix: add FROM fm.shop:
     CREATE OR REPLACE FEATURES IN fm.shop AS
     SELECT
         ORDER_COUNT := CUSTOMER_ID.RELATED(COUNT(1) GROUP BY FCT_ORDERS[customer_id])
     FROM fm.shop;
null

7.2 Feature name collision in SELECT AS

WITH
    NEXT_PAGE := LEAD(HIT_PAGE, 1) OVER (ORDER BY HIT_NUMBER ASC),
SELECT
    FILTERED_NEXT AS NEXT_PAGE,
sql

Intent: rename a computed feature in the output.

Problem: SELECT alias AS NEXT_PAGE creates a feature called NEXT_PAGE, which already exists in WITH.

Error: [EXISTS] UE/DUPLICATE_FEATURE_DEFINITION

Error UE/DUPLICATE_FEATURE_DEFINITION at line 4

  FILTERED_NEXT AS NEXT_PAGE
                   ^^^^^^^^^
  'NEXT_PAGE' is already defined at line 2.
  SELECT ... AS creates a feature with that name, which collides.

Fix: rename either the WITH feature or the SELECT alias:
     WITH HIT_NEXT_PAGE := LEAD(...),
     or: SELECT FILTERED_NEXT AS OUTPUT_NEXT_PAGE,
null

8. EXTEND and array errors

8.1 Using ZIP on an array that is already ARRAY(ROW(...))

-- ORDER_ITEMS is ARRAY(ROW(product_id BIGINT#PRODUCTS, quantity BIGINT))
ENRICHED := EXTEND(
    ZIP(ORDER_ITEMS AS product_id)
    WITH PRODUCT_NAME
    VIA product_id BIND TO PRODUCT_ID
),
sql

Intent: enrich line items with product name.

Problem: ORDER_ITEMS is already an array of rows. ZIP is for scalar arrays (ARRAY(BIGINT#X)). Wrapping a ROW array in ZIP drops all fields except the one you name.

Error: [NEW] UE/EXTEND-ZIP-ON-ROW-ARRAY

Error UE/EXTEND-ZIP-ON-ROW-ARRAY at line 3

  ZIP(ORDER_ITEMS AS product_id)
      ^^^^^^^^^^^
  ORDER_ITEMS is already ARRAY(ROW(product_id BIGINT#PRODUCTS, quantity BIGINT)).
  ZIP is for scalar arrays like ARRAY(BIGINT#PRODUCTS).
  Using ZIP here would discard the 'quantity' field.

Fix: pass the array of rows directly:
     ENRICHED := EXTEND(
         ORDER_ITEMS
         WITH PRODUCT_NAME
         VIA product_id BIND TO PRODUCT_ID
     ),
null

8.2 Missing ZIP on a scalar array before EXTEND

-- CUSTOMER_ORDER_IDS is ARRAY(BIGINT#ORDERS)
ENRICHED := EXTEND(
    CUSTOMER_ORDER_IDS
    WITH ORDER_DATE, ORDER_AMOUNT
    VIA order_id BIND TO ORDER_ID
),
sql

Intent: enrich a list of order IDs with order data.

Problem: CUSTOMER_ORDER_IDS is ARRAY(BIGINT#ORDERS), not an array of rows. EXTEND needs rows with named fields. Use ZIP to convert the scalar array into an array of rows first.

Error: [NEW] UE/EXTEND-REQUIRES-ROW-ARRAY

Error UE/EXTEND-REQUIRES-ROW-ARRAY at line 3

  EXTEND(CUSTOMER_ORDER_IDS WITH ...)
         ^^^^^^^^^^^^^^^^^^
  CUSTOMER_ORDER_IDS is ARRAY(BIGINT#ORDERS), not ARRAY(ROW(...)).
  EXTEND requires an array of rows so it can add fields.

Fix: wrap in ZIP to create a named field:
     EXTEND(
         ZIP(CUSTOMER_ORDER_IDS AS order_id)
         WITH ORDER_DATE, ORDER_AMOUNT
         VIA order_id BIND TO ORDER_ID
     ),
null

9. SQL thinking mistakes (users who think in SQL)

9.1 Trying to use a CTE or subquery

WITH cte AS (
    SELECT customer_id, SUM(amount) as total
    FROM orders
    GROUP BY customer_id
)
SELECT * FROM cte;
sql

Intent: compute aggregated data using SQL patterns.

Problem: FeatureQL has no CTEs, no subqueries, no FROM table at the top level. It is a flat list of feature definitions.

Error: [EXISTS] UE/SYNTAX-ERROR

Error UE/SYNTAX-ERROR at line 1

  WITH cte AS (
           ^^
  Unexpected token 'AS'. FeatureQL WITH defines features, not CTEs.

  FeatureQL syntax:
    WITH feature_name := expression,
  Not:
    WITH name AS (SELECT ...)

Fix: think in features, not CTEs:
     WITH
         CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
         TOTAL := CUSTOMER_ID.RELATED(
             SUM(ORDER_AMOUNT) GROUP BY FCT_ORDERS[customer_id]
         ),
     SELECT CUSTOMER_ID, TOTAL
     FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2, 3]);
null

9.2 GROUP BY as a separate clause at the end

SELECT
    CATEGORY,
    TOTAL := SUM(AMOUNT),
FROM fm.shop
FOR ID := BIND_VALUES(SEQUENCE(1, 100))
GROUP BY CATEGORY;
sql

Intent: aggregate by category.

Problem: FeatureQL GROUP BY is per feature, attached to each aggregate, not a trailing clause.

Error: [EXISTS] UE/SYNTAX-ERROR

Error UE/SYNTAX-ERROR at line 6

  GROUP BY CATEGORY;
  ^^^^^^^^
  Unexpected GROUP BY after FOR. In FeatureQL, GROUP BY is attached
  to each aggregate, not a separate trailing clause.

Fix:
  SELECT
      CATEGORY,
      TOTAL := SUM(AMOUNT) GROUP BY CATEGORY,
  ...
null

9.3 Using ILIKE (does not exist)

MATCHES := CAMPAIGN_NAME ILIKE '%summer%',
sql

Intent: case-insensitive pattern matching.

Problem: ILIKE is not in the FeatureQL grammar.

Error: [EXISTS] UE/SYNTAX-ERROR

Error UE/SYNTAX-ERROR at line 1, col 28

  MATCHES := CAMPAIGN_NAME ILIKE '%summer%',
                           ^^^^^
  'ILIKE' is not a recognized operator in FeatureQL.

Fix: use LOWER() + LIKE for case-insensitive matching:
     MATCHES := LOWER(CAMPAIGN_NAME) LIKE '%summer%',

     or LIKE for case-sensitive matching:
     MATCHES := CAMPAIGN_NAME LIKE '%Summer%',
null

9.4 Using SQL-style BIGINT[] instead of ARRAY(BIGINT)

DIM := EXTERNAL_COLUMNS(
    customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
    order_ids BIGINT[]
    FROM TABLE(schema.customers_obt)
),
sql

Intent: declare an array column.

Problem: BIGINT[] is SQL array syntax. FeatureQL uses ARRAY(BIGINT#ORDERS).

Error: [EXISTS] UE/SYNTAX-ERROR

Error UE/SYNTAX-ERROR at line 3, col 14

  order_ids BIGINT[]
                  ^^
  '[]' is not valid type syntax. FeatureQL uses ARRAY(TYPE) for array types.

Fix: order_ids ARRAY(BIGINT#ORDERS)
     (include the #ENTITY annotation if the values are entity keys)
null

10. Dual FK and advanced relationship errors

10.1 Only one RELATED for a dual-FK table

-- fct_flights has flight_origin_id and flight_dest_id, both referencing AIRPORTS
ORIGIN_CITY := RELATED(AIRPORT_CITY VIA TABLES.FCT_FLIGHTS[flight_origin_id]),
DEST_CITY := RELATED(AIRPORT_CITY VIA TABLES.FCT_FLIGHTS[flight_origin_id]),  -- copy-paste mistake
sql

Intent: get origin and destination cities for a flight.

Problem: both RELATED calls use flight_origin_id. The second should use flight_dest_id. This is a copy-paste bug, not a type error, so the engine cannot catch it at compile time. However, if the result looks wrong (both cities are identical), the trace should help.

Error: No compile error. This is a logic bug caught at review time.

Hint: both ORIGIN_CITY and DEST_CITY resolve through the same FK column
      (TABLES.FCT_FLIGHTS[flight_origin_id]). If you have two FK columns
      to the same entity, each RELATED needs a different VIA path:

      ORIGIN_CITY := RELATED(AIRPORT_CITY VIA TABLES.FCT_FLIGHTS[flight_origin_id]),
      DEST_CITY   := RELATED(AIRPORT_CITY VIA TABLES.FCT_FLIGHTS[flight_dest_id]),
null

10.2 Forgetting COALESCE on reverse aggregation with dual FK

NUM_DEPARTURES := AIRPORT_ID.RELATED(COUNT(1) GROUP BY TABLES.FCT_FLIGHTS[flight_origin_id]),
NUM_ARRIVALS := AIRPORT_ID.RELATED(COUNT(1) GROUP BY TABLES.FCT_FLIGHTS[flight_dest_id]),
TOTAL_TRAFFIC := NUM_DEPARTURES + NUM_ARRIVALS,
sql

Intent: count total flights through an airport (departures + arrivals).

Problem: an airport with zero departures (or zero arrivals) returns NULL for that RELATED count, not 0. NULL + 5 = NULL.

Error: No compile error. This is a runtime data bug.

Hint: RELATED(COUNT ...) returns NULL when no rows match, not 0.
      If an airport has departures but no arrivals (or vice versa),
      TOTAL_TRAFFIC will be NULL.

Fix: TOTAL_TRAFFIC := COALESCE(NUM_DEPARTURES, 0) + COALESCE(NUM_ARRIVALS, 0),
null

11. UNNEST errors

11.1 Mixing unnested and non-unnested features in SELECT

SELECT
    CUSTOMER_ID,
    UNNEST(ORDERS) AS U,
    ORDER_ID := U[order_id],
    CUSTOMER_NAME,             -- not derived from UNNEST
FOR CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]);
sql

Intent: flatten orders and keep customer name alongside.

Problem: UNNEST is exclusive. You can only return unnested features and features derived from them. CUSTOMER_NAME is at customer grain, not unnested grain.

Error: [NEW] UE/UNNEST-MIXED-GRAIN

Error UE/UNNEST-MIXED-GRAIN at line 5

  CUSTOMER_NAME,
  ^^^^^^^^^^^^^
  CUSTOMER_NAME is at CUSTOMERS grain, but SELECT uses UNNEST which
  changes the grain to element-per-row. You cannot mix unnested
  and non-unnested features in the same SELECT.

Fix: either CARRY the customer-level value onto each order row before
     unnesting, or split into two separate queries.
null

12. Metaprogramming errors

12.1 Using CURRENT_TIMESTAMP() without @eval_as_literal

WITH
    TODAY := CURRENT_TIMESTAMP(),
    DAYS_SINCE := DATE_DIFF(TODAY, order_date, 'DAY'),
sql

Intent: compute days since each order relative to "now".

Problem: without @eval_as_literal(...), CURRENT_TIMESTAMP() is evaluated per row. This is usually not what you want for a "snapshot" date, and can cause inconsistencies within a single query execution.

Error: No compile error. This is a semantic warning.

Warning: CURRENT_TIMESTAMP() without @eval_as_literal is evaluated per row.
         If you want a single snapshot timestamp for the whole query:
         TODAY := @eval_as_literal(CURRENT_TIMESTAMP()),

         Per-row evaluation can return different timestamps across rows
         and is almost never the intended behavior for "today's date".
null

Summary of error codes

Existing codes referenced

CodeCategory
UE/CASE_WHEN-MIXED-TYPESType system
UE/COMPARISON-TYPE-EMPTYType system
UE/COMPARISON-DIGITS-EMPTYType system
UE/SYNTAX-ERRORSyntax
UE/FEATURE-NOT-FOUNDNamespace / scope
UE/DUPLICATE_FEATURE_DEFINITIONNamespace
UE/EXTERNAL-COLUMNS-CSV-NO-DATAData source
UE/EXTEND-UNRESOLVABLE-FEATURESRELATED
UE/AGGREGATE-GROUP-BY-NOT-IDENTIFIERAggregation
UE/AGGREGATE-WITH-REPORTAggregation
UE/TRANSFORM-IMPORT-FEATURE-NOT-PURETRANSFORM

Proposed new codes

CodeCategorySection
UE/ARRAY-MIXED-TYPE-FAMILIESType system1.2
UE/BETWEEN-TYPE-MISMATCHType system1.3
UE/MISSING-BINDINGBinding2.1
UE/UNBOUND-TRANSITIVE-INPUTBinding2.2
UE/CROSS-PRODUCT-REQUIRES-FOR-CROSSBinding2.3
UE/BIND-TO-ENTITY-MISMATCHEntity3.1
UE/ENTITY-NOT-DECLAREDEntity3.2
UE/EXTERNAL-COLUMNS-MULTIPLE-BIND-TOData source3.3
UE/RELATED-VIA-WRONG-DIRECTIONRelationship3.4
UE/TRANSFORM-UNKNOWN-FIELDTRANSFORM4.1
UE/TRANSFORM-OUTER-FEATURE-NOT-IN-SCOPETRANSFORM4.2
UE/EXTERNAL-COLUMNS-NO-BIND-TOData source5.2
UE/EXTEND-ZIP-ON-ROW-ARRAYEXTEND8.1
UE/EXTEND-REQUIRES-ROW-ARRAYEXTEND8.2
UE/UNNEST-MIXED-GRAINUNNEST11.1
Last update at: 2026/04/30 17:22:21