Problem
I have orders and products, and I want each order enriched with the product it refers to — price, category, the lot. Inner join territory. Also: which orders don't have a matching product, since Morel 0.8 hasn't got outer joins yet.
Setup
val orders = [
{ id = 1, productId = 101, quantity = 12, region = "north" },
{ id = 2, productId = 204, quantity = 30, region = "south" },
{ id = 4, productId = 204, quantity = 50, region = "west" },
{ id = 5, productId = 101, quantity = 6, region = "east" },
{ id = 8, productId = 102, quantity = 25, region = "west" }
];
val products = [
{ id = 101, name = "Earl Grey", category = "tea", price = 18.50 },
{ id = 102, name = "Darjeeling", category = "tea", price = 24.00 },
{ id = 204, name = "Ethiopia", category = "coffee", price = 28.00 }
];
Example
Join each order to its product and compute a line total:
from ord in orders, p in products
where ord.productId = p.id
yield { ord.id, p.name, ord.quantity, lineTotal = real ord.quantity * p.price };
val it =
[{id=1,lineTotal=222.0,name="Earl Grey",quantity=12},
{id=2,lineTotal=840.0,name="Ethiopia",quantity=30},
{id=4,lineTotal=1400.0,name="Ethiopia",quantity=50},
{id=5,lineTotal=111.0,name="Earl Grey",quantity=6},
{id=8,lineTotal=600.0,name="Darjeeling",quantity=25}]
: {id:int, lineTotal:real, name:string, quantity:int} list
What's happening
Two scans in the from list — ord in orders, p in products — form
a cross product, and the where clause narrows it to the pairs where
the join condition holds. That's the ANSI-SQL mental model and it
works in Morel too. The yield step can reach into either side
because both names are still in scope.
There's also a dedicated join step with an inline on clause,
which reads more like modern SQL and keeps the condition close to the
scan it belongs with. Either form compiles to the same thing; pick
the one that reads better for the query.
On missing pieces: Morel 0.8 does not yet implement outer joins
(issue 75). For "all
orders including unmatched ones" you'd do it in two passes and
concatenate — or, as below, compute just the unmatched set with an
exists subquery. The limitation is worth knowing about before you
design a report around it.
Variations
Same join, written with an explicit join step:
from ord in orders
join p in products on ord.productId = p.id
yield { ord.id, p.category };
Orders whose product doesn't exist in the products table — Morel's
answer to "left join where right is null". Note exists is a
top-level expression, like from, and takes its own scan:
from ord in orders
where not (exists p in products where p.id = ord.productId)
yield { ord.id, ord.productId };
See also
- Recipe 07 — Filter rows —
wheredoes double duty as filter and join condition. - Recipe 10 — Group and aggregate — group by a joined-in column (category, supplier).
- Recipe 17 — Define a metric once — the
lineTotalcomputation as a reusable named function.