Skip to main content

join

The join strategy enriches rows from a primary collection with fields from one or more joined collections. It compiles to parallel scans, a join, a field projection, a sort, and a page.

Compiles to: scan + scan → join → project → sort → page

Use this strategy when each row in your result needs fields from another collection — for example, displaying a list of messages alongside the author's name and avatar from a users collection.

Example

domain:
projections:
messages_with_authors:
strategy: join
primary:
collection: hello_messages
as: m
where:
"==":
- var: row.user_id
- var: principal.subject
join:
- collection: users
as: u
type: left
on:
"==":
- var: m.user_id
- var: u.id
fields:
- u.name as author_name
- u.avatar_url as author_avatar_url
fields:
- m.id
- m.message
- m.created_at
- m.user_id
- author_name
- author_avatar_url
search:
arg: q
fields: [m.message, author_name]
sort:
by_arg: sort_by
order_arg: sort_order
allowed: [m.created_at, author_name]
default_by: m.created_at
default_order: desc
pagination:
mode: page
page_arg: page
page_size_arg: page_size
default_page_size: 10
max_page_size: 50

primary

The primary block names the primary collection and assigns it an alias. The alias is used to disambiguate field references when multiple collections share a field name.

primary:
collection: hello_messages
as: m

join

The join array declares one or more collections to join against the primary.

join:
- collection: users
as: u
type: left
on:
"==":
- var: m.user_id
- var: u.id
fields:
- u.name as author_name
- u.avatar_url as author_avatar_url
FieldRequiredDescription
collectionYesThe collection to join
asYesAlias for this collection's fields
typeYesleft or inner
onYes$expr join condition
fieldsYesFields to extract from this collection into the result row

left join — every row from the primary is returned, even if no matching row exists in the joined collection. Joined fields are null for non-matching rows.

inner join — only rows with a matching record in the joined collection are returned.

Field aliasing

Fields extracted from joined collections must be given an alias.

fields:
- u.name as author_name
- u.avatar_url as author_avatar_url

The alias becomes the field name in the result row and in subsequent search, sort, and where expressions.

fields

The fields array declares the final output shape of the projection. Only listed fields appear in the result.

fields:
- m.id
- m.message
- m.created_at
- m.user_id
- author_name # from the join above
- author_avatar_url # from the join above

Fields from the primary collection use <alias>.<field> notation. Joined fields are referenced by their alias (no collection prefix).

where

where on a join projection filters rows after the join is applied. It can reference fields from both the primary and joined collections using their aliases.

where:
and:
- "==":
- var: m.user_id
- var: principal.subject
- "!==":
- var: author_name
- null

search applies full-text filtering across fields from any collection in the join.

search:
arg: q
fields: [m.message, author_name]

sort and pagination

Sort and pagination work identically to from_collection. Field references in sort.allowed use the same alias syntax as fields.

Multiple joins

The join array supports multiple entries. Each entry is evaluated against the primary row in the order declared.

join:
- collection: users
as: u
type: left
on:
"==": [{ var: m.user_id }, { var: u.id }]
fields:
- u.name as author_name

- collection: channels
as: c
type: inner
on:
"==": [{ var: m.channel_id }, { var: c.id }]
fields:
- c.name as channel_name

What's next

  • aggregate: group joined rows and compute metrics
  • timeline: build read model state from signal history
  • from_collection: simpler strategy for single-collection reads