AQL Validation Rules
This page lists the constraints AQL enforces. These rules either fail validation or produce wrong results when broken. For advisory guidance on writing maintainable AQL, see AQL Best Practices. For the mental model of when filters and metrics fire, see Order of Operations.
Model rules
Dimension types must match the underlying column type
Mismatched types cause runtime errors and incorrect results. Holistics does not auto-cast because consistent casting between incompatible types (text to datetime, for example) isn't always possible. Silent casts also hide performance and correctness problems.
// ✅ Good
dimension created_at {
type: 'datetime' // matches timestamp column
}
// ❌ Bad
dimension created_at {
type: 'text' // timestamp column declared as text
}
Relationship rules
Relationships must match the actual data cardinality
Marking a many-to-many as many-to-one produces wrong aggregations (fan-out, double-counting). Verify cardinality against the data, not against your assumption of the schema.
// ✅ Good: verified many-to-one
relationship(orders.user_id > users.id, true)
// ❌ Bad: actually many-to-many
relationship(users.id > products.id, true)
Both keys referenced in a relationship must exist
Missing keys fail validation.
// ✅ Good
Model orders { dimension user_id { type: 'number' } }
Model users { dimension id { type: 'number', primary_key: true } }
relationship(orders.user_id > users.id, true)
// ❌ Bad: customer_id doesn't exist on orders
relationship(orders.customer_id > users.id, true)
The "one" side of a relationship must have unique values
Fan-out prevention relies on uniqueness of the key on the "one" side. Non-unique keys produce incorrect aggregations.
// ✅ Good: cities.id is a primary key
relationship(users.city_id > cities.id, true)
// ❌ Bad: cities.name isn't unique (many cities share names)
relationship(users.city_name > cities.name, true)
Relationship paths between models must be unambiguous
Multiple paths between the same pair of models create ambiguity AQL can't resolve. See Common Relationship Problems for worked examples.
// ❌ Bad: circular reference creates two paths between orders and users
relationship(orders.user_id > users.id, true)
relationship(users.current_order_id > orders.id, true)
// ✅ Good: clear hierarchy
relationship(orders.user_id > users.id, true)
relationship(order_items.order_id > orders.id, true)
Dimension expression rules
Dimensions can only reference fields from accessible models
A dimension can reach its own model's fields plus any model connected via a many-to-one (or chain of many-to-one) relationship. Reaching the "many" side directly fails.
// ✅ Good: direct many-to-one
dimension customer_name {
model: orders
definition: @aql users.name ;;
}
// ✅ Good: transitive many-to-one chain
dimension customer_city {
model: orders
definition: @aql cities.name ;; // orders → users → cities
}
// ❌ Bad: trying to reach the many side
dimension user_orders {
model: users
definition: @aql orders.id ;; // a user has many orders
}
// ❌ Bad: no relationship path
dimension product_category {
model: users
definition: @aql products.category ;;
}
Use dimensionalize() to access the many side of a relationship
To pull aggregated information from the "many" side into a dimension, wrap with dimensionalize(). This collapses the aggregation to a fixed grain so the result behaves as a dimension.
// ✅ Good
dimension total_spent {
model: users
definition: @aql sum(orders.amount) | dimensionalize(users.id) ;;
}
// ❌ Bad: direct access to many side
dimension last_order_amount {
model: users
definition: @aql orders.amount ;;
}
dimensionalize() must appear once, at the top level
Nesting dimensionalize() inside other expressions or applying it more than once produces undefined behavior.
// ✅ Good
dimension customer_lifetime_value {
model: users
definition: @aql sum(orders.amount) | dimensionalize(users.id) ;;
}
// ❌ Bad: nested dimensionalize
dimension complex_calc {
model: users
definition: @aql
sum(orders.amount) | dimensionalize(users.id) +
count(reviews.id) | dimensionalize(users.id)
;;
}
// ❌ Bad: dimensionalize not at top level
dimension avg_order_by_category {
model: categories
definition: @aql
avg(sum(orders.amount) | dimensionalize(users.id))
;;
}
Metric expression rules
Cross-model aggregations must specify a source table
When the expression combines fields from multiple models, AQL can't infer which table sets the aggregation grain. State it explicitly.
// ✅ Good: explicit source table
metric revenue {
definition: @aql sum(order_items, order_items.quantity * products.price) ;;
}
// ❌ Bad: ambiguous source
metric revenue {
definition: @aql sum(order_items.quantity * products.price) ;;
}
group() must be followed by select() or filter()
Aggregate functions can technically follow group() directly, but it raises WARN-300 and obscures the intermediate table. Use select() or filter() to keep the grouped table explicit.
// ✅ Good
metric avg_items_per_order {
definition: @aql
orders
| group(orders.id)
| select(item_count: count(order_items.id))
| avg(item_count)
;;
}
// ⚠️ Triggers WARN-300
metric avg_items_per_order {
definition: @aql
orders
| group(orders.id)
| avg(count(order_items.id))
;;
}
Fields referenced in LoD functions must exist and be in scope
of_all(), exclude(), and dimensionalize() arguments must resolve to a real dimension reachable from the current model context.
// ✅ Good
metric percent_of_total {
definition: @aql safe_divide(revenue * 100, revenue | of_all(products)) ;;
}
// ❌ Bad: 'product' doesn't exist; the model is 'products'
metric percent_of_total {
definition: @aql safe_divide(revenue * 100, revenue | of_all(product)) ;;
}
See also
- AQL Best Practices: advisory guidance and pitfalls
- Order of Operations: how AQL evaluates filters, metrics, and LoD
- Error Reference: full list of validation error codes
- Common Relationship Problems: debugging ambiguous paths and fan-out