Relationship
A Relationship defines a join path between two models. Once declared, the engine resolves the join automatically whenever a query traverses a relationship column — no explicit JOIN syntax required.
Structure
# relationships.yml
relationships:
- name: orders_customers
models:
- orders
- customers
join_type: MANY_TO_ONE
condition: orders.customer_id = customers.customer_id
JSON format (MDL manifest)
{
"name": "orders_customers",
"models": ["orders", "customers"],
"joinType": "MANY_TO_ONE",
"condition": "orders.customer_id = customers.customer_id"
}
Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier referenced by relationship columns |
models | Yes | Exactly two model names: [from_model, to_model] |
join_type | Yes | Cardinality of the join (see below) |
condition | Yes | SQL equality expression linking the two models |
Join Types
| Value | Meaning |
|---|---|
ONE_TO_ONE | Each row in the left model matches at most one row in the right model |
ONE_TO_MANY | One row in the left model matches many rows in the right model |
MANY_TO_ONE | Many rows in the left model match one row in the right model |
MANY_TO_MANY | Many rows on both sides |
The join type affects how the engine handles aggregation in calculated columns that traverse the relationship. For TO_ONE joins (ONE_TO_ONE, MANY_TO_ONE), the engine uses a simple join. For TO_MANY joins, the engine wraps the traversal in an aggregate subquery to avoid row multiplication.
The condition Field
The condition is an equality expression using fully-qualified model.column references:
condition: orders.customer_id = customers.customer_id
- Always use
model_name.column_nameon both sides - Only equality conditions are supported
- The first model in
modelsshould appear on the left side of the condition
jaffle_shop Example
The jaffle_shop workspace defines five relationships across its three model layers:
relationships:
# mart layer
- name: orders_customers
models: [orders, customers]
join_type: MANY_TO_ONE
condition: orders.customer_id = customers.customer_id
# raw layer
- name: raw_orders_raw_customers
models: [raw_orders, raw_customers]
join_type: MANY_TO_ONE
condition: raw_orders.user_id = raw_customers.id
- name: raw_payments_raw_orders
models: [raw_payments, raw_orders]
join_type: MANY_TO_ONE
condition: raw_payments.order_id = raw_orders.id
# staging layer
- name: stg_orders_stg_customers
models: [stg_orders, stg_customers]
join_type: MANY_TO_ONE
condition: stg_orders.customer_id = stg_customers.customer_id
- name: stg_payments_stg_orders
models: [stg_payments, stg_orders]
join_type: MANY_TO_ONE
condition: stg_payments.order_id = stg_orders.order_id
Using Relationships in Queries
Implicit join via relationship column
Declare a relationship column in a model to expose a join path:
# orders model — add a relationship column pointing to customers
columns:
- name: customer
type: customers
relationship: orders_customers
Then query across models without writing a JOIN:
-- Wren expands the join automatically
SELECT order_id, customer.first_name, customer.last_name, amount
FROM orders
WHERE customer.number_of_orders > 3
ORDER BY amount DESC;
The engine resolves customer.* by expanding the orders_customers join, pushing it only as far as the referenced columns require.
Calculated columns that traverse relationships
Relationship columns can be referenced inside is_calculated expressions:
- name: customer_name
type: VARCHAR
is_calculated: true
expression: "customer.first_name || ' ' || customer.last_name"
relationship: orders_customers
For TO_MANY relationships, aggregate functions are required to avoid row multiplication:
# In the customers model — count orders per customer
- name: order_count
type: BIGINT
is_calculated: true
expression: "count(orders.order_id)"
relationship: orders_customers
The engine detects the aggregate and automatically wraps the join in a subquery.
Engine Internals
Relationship resolution pipeline
When the query planner encounters a column reference like orders.customer.first_name:
ExpandWrenViewRuleruns first to inline any view definitionsModelAnalyzeRuleidentifies thecustomercolumn as a relationship column pointing toorders_customersrelation_chainresolves the join path, building aLEFT JOIN customers ON orders.customer_id = customers.customer_id- The join is pushed only as far as the referenced columns require — unreferenced relationship columns do not produce joins
TO_MANY and aggregate subqueries
The primary_key of the base model is required when the relationship is TO_MANY. The engine wraps the join in an aggregate subquery keyed on the primary key to prevent row multiplication:
-- expression: count(orders.order_id) on customers model
SELECT
customers.customer_id,
(SELECT count(orders.order_id)
FROM orders
WHERE orders.customer_id = customers.customer_id) AS order_count
FROM customers
If primary_key is not declared on the base model, the engine returns an error when a TO_MANY calculated column is used.