Joins¶
Read-only joins on find. Inner and left types, chainable, by the remote object's primary key or any indexed field. Output is always tabular.
Shape¶
{
"mode": "find",
"dir": "acme",
"object": "orders",
"criteria": [{"field":"status","op":"eq","value":"paid"}],
"join": [
{
"object": "users",
"local": "user_id",
"remote": "key",
"as": "user",
"type": "inner",
"fields": ["email","name"]
},
{
"object": "products",
"local": "product_sku",
"remote": "sku",
"as": "product",
"type": "left",
"fields": ["title","price"]
}
],
"limit": 50
}
Per-join options¶
| Field | Required | Meaning |
|---|---|---|
object |
yes | Remote object to join. Must live in the same dir as the driver. |
local |
yes | Driver-side field whose value is looked up in the remote object. Supports composite ("country+zip") when the remote has a matching composite index. |
remote |
yes | Either "key" (primary-key lookup, O(1) hash) or any indexed field on the remote object (O(log n) B+ tree). Unindexed fields are rejected at parse time. |
as |
yes | Column prefix in the output. Must be unique — can't collide with the driver object name or other as values. |
type |
no (default "inner") |
"inner" drops driver rows without a remote match; "left" emits nulls instead. |
fields |
no (default all) | Remote fields to include in the output. Tombstoned fields are skipped. |
Output shape¶
Always tabular:
{
"columns": [
"orders.key", "orders.amount", "orders.status", "orders.user_id",
"user.email", "user.name",
"product.title", "product.price"
],
"rows": [
["o1", 99.50, "paid", "u42", "a@b.c", "Ana", "Widget", 9.99],
["o2", 39.00, "paid", "u51", null, null, "Gadget", 19.50]
]
}
- Driver columns are prefixed with the driver object name (
orders.*). - Each join's columns are prefixed with
as(user.*,product.*). - Left-join no-match emits
nullfor all the join's columns. keyis always included for the driver; joined objects only emit the fields you list (or all iffieldsis omitted).
The format parameter is ignored for the JSON shape (always tabular). format:"csv" with join is rejected with format=csv is not supported with join — joins always emit the JSON {columns, rows} envelope. cursor pagination is also not supported with join; use offset/limit.
Chaining¶
Joins apply in declaration order. Each subsequent join uses the same driver record; there's no "join a join" nesting. If you need two hops, chain them as two joins — both off the driver — not as nested joins.
Limit behavior¶
- Inner-only (or no) joins —
limitis applied after the join. Records that inner-drop don't count toward the limit. You get up tolimitrows that fully matched. - Left joins —
limitis applied during collection. Every match (including left-null fallbacks) counts.
Sorting¶
Not supported with join. Sort in the app after fetching, or pre-filter the driver with sufficient selectivity that the result set fits a reasonable limit.
Cost¶
remote: "key"— O(1) hash lookup per driver row. As fast asgetper record.remote: "<indexed_field>"— O(log n) B+ tree lookup per driver row, with a typed-equality filter.- Composite
local(e.g.,"country+zip") requires the remote object to have a matching composite index.
For high-fanout joins (every driver row matches many remote rows), use pagination + projection aggressively — there's no streaming response, everything buffers before flush.
Recipes¶
Enrich orders with customer + product¶
{"mode":"find","dir":"acme","object":"orders",
"criteria":[{"field":"status","op":"eq","value":"paid"}],
"join":[
{"object":"customers","local":"customer_id","remote":"key",
"as":"cust","fields":["name","email","tier"]},
{"object":"products","local":"product_sku","remote":"sku",
"as":"prod","fields":["title","price"]}
],
"limit":100}
Left join to detect missing data¶
{"mode":"find","dir":"default","object":"orders",
"criteria":[{"field":"status","op":"eq","value":"paid"}],
"join":[
{"object":"users","local":"user_id","remote":"key",
"as":"user","type":"left","fields":["email"]}
]}
Rows where user.email is null are orders referencing a user that's been deleted.
Composite join¶
Driver has country_code and zip_code; remote has a composite index on "country+zip":
{"mode":"find","dir":"acme","object":"deliveries",
"join":[
{"object":"regions","local":"country_code+zip_code","remote":"country+zip",
"as":"region","fields":["name","timezone"]}
]}
Limitations¶
- Read-only. No joined writes or updates.
- Same-
dironly. Cross-tenant joins are not supported. - No nested/subquery joins — join targets are base objects, not query results.
- No right or full outer joins. Use left and reorder.
formatforced to tabular — JSON-record form is not produced whenjoinis present.