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:
- What the user wrote (the broken query)
- What they meant (the intent)
- Error code and message (what the engine should return)
- 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); 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) 1.2 BIGINT and DECIMAL mixed in ARRAY constructor
WITH
VALUES := ARRAY[1.25, 1],
SELECT VALUES; 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] 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)]); 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 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')]); 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 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]); 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 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]); 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) 2. Binding and INPUT errors
2.1 Missing FOR when SELECT depends on INPUT
WITH
ID := INPUT(BIGINT),
DOUBLED := ID * 2,
SELECT ID, DOUBLED; 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]); 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]); 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. 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]); 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)]); 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)
), 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, 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]); 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), 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)
), 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. 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]), 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) 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]); 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)) 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]); 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), 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
), 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)), 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)
), 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"}]) 5.2 Missing BIND TO in EXTERNAL_COLUMNS
DIM_CUSTOMERS := EXTERNAL_COLUMNS(
customer_id BIGINT#CUSTOMERS,
name VARCHAR
FROM TABLE(schema.dim_customers)
), 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, 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)
), 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, 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]
), 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. 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]
), 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]) 6.3 GROUP BY on a literal value
GLOBAL_TOTAL := SUM(ORDER_AMOUNT) GROUP BY 1, 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) 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)); 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 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]); 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; 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, 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, 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
), 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
), 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
), 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
), 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; 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]); 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; 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,
... 9.3 Using ILIKE (does not exist)
MATCHES := CAMPAIGN_NAME ILIKE '%summer%', 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%', 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)
), 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) 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 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]), 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, 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), 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]); 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. 12. Metaprogramming errors
12.1 Using CURRENT_TIMESTAMP() without @eval_as_literal
WITH
TODAY := CURRENT_TIMESTAMP(),
DAYS_SINCE := DATE_DIFF(TODAY, order_date, 'DAY'), 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". Summary of error codes
Existing codes referenced
| Code | Category |
|---|---|
UE/CASE_WHEN-MIXED-TYPES | Type system |
UE/COMPARISON-TYPE-EMPTY | Type system |
UE/COMPARISON-DIGITS-EMPTY | Type system |
UE/SYNTAX-ERROR | Syntax |
UE/FEATURE-NOT-FOUND | Namespace / scope |
UE/DUPLICATE_FEATURE_DEFINITION | Namespace |
UE/EXTERNAL-COLUMNS-CSV-NO-DATA | Data source |
UE/EXTEND-UNRESOLVABLE-FEATURES | RELATED |
UE/AGGREGATE-GROUP-BY-NOT-IDENTIFIER | Aggregation |
UE/AGGREGATE-WITH-REPORT | Aggregation |
UE/TRANSFORM-IMPORT-FEATURE-NOT-PURE | TRANSFORM |
Proposed new codes
| Code | Category | Section |
|---|---|---|
UE/ARRAY-MIXED-TYPE-FAMILIES | Type system | 1.2 |
UE/BETWEEN-TYPE-MISMATCH | Type system | 1.3 |
UE/MISSING-BINDING | Binding | 2.1 |
UE/UNBOUND-TRANSITIVE-INPUT | Binding | 2.2 |
UE/CROSS-PRODUCT-REQUIRES-FOR-CROSS | Binding | 2.3 |
UE/BIND-TO-ENTITY-MISMATCH | Entity | 3.1 |
UE/ENTITY-NOT-DECLARED | Entity | 3.2 |
UE/EXTERNAL-COLUMNS-MULTIPLE-BIND-TO | Data source | 3.3 |
UE/RELATED-VIA-WRONG-DIRECTION | Relationship | 3.4 |
UE/TRANSFORM-UNKNOWN-FIELD | TRANSFORM | 4.1 |
UE/TRANSFORM-OUTER-FEATURE-NOT-IN-SCOPE | TRANSFORM | 4.2 |
UE/EXTERNAL-COLUMNS-NO-BIND-TO | Data source | 5.2 |
UE/EXTEND-ZIP-ON-ROW-ARRAY | EXTEND | 8.1 |
UE/EXTEND-REQUIRES-ROW-ARRAY | EXTEND | 8.2 |
UE/UNNEST-MIXED-GRAIN | UNNEST | 11.1 |