7.10.16. SQL-15 — _join (inner equi-join)
_join(other, on, into) is sugar over the existing
daslib/linq.das::join function — under _sql(...) it emits
SQL’s INNER JOIN ... ON ....
7.10.16.1. Equi-join shape
The on predicate is locked to the equi-join shape
$(l : TA, r : TB) => l.X == r.Y
— a 2-arg lambda whose body is one == comparison between a
left-side field and a right-side field. Theta joins (<, >,
&&-chained multi-key equality, function calls inside) emit a
compile-time error[50503] pointing at raw SQL as the escape
hatch. The single-key restriction matches the backing join fn’s
hash-based O(n+m) implementation; for composite-key joins, use raw
SQL.
7.10.16.2. Multi-source mode and table aliases
In multi-source mode the FROM clause aliases the root table as
t0, the joined table as t1, and every column ref in the
WHERE, projection, and ON clauses qualifies with the matching alias.
Single-source chains keep the unqualified shape — only multi-source
queries pay the alias-noise tax.
let rows <- _sql(db |> select_from(type<User>)
|> _join(db |> select_from(type<Order>),
$(u : User, o : Order) => u.Id == o.UserId,
$(u : User, o : Order) => (UserName = u.Name, Total = o.Total)))
// SELECT "t0"."Name", "t1"."Total"
// FROM "Users" AS "t0"
// INNER JOIN "Orders" AS "t1"
// ON "t0"."Id" = "t1"."UserId"
7.10.16.3. Filtering before / inside / after the join
Pre-join _where filters the LEFT source. Column refs qualify
with the left alias automatically:
db |> select_from(type<User>)
|> _where(_.Active)
|> _join(...)
// ... WHERE "t0"."Active"
_where can also live inside the right-side chain — filters
there qualify with the right alias t1 and emit into the JOIN’s
ON clause (not the outer WHERE):
_join(db |> select_from(type<Order>) |> _where(_.Total > 75), ...)
// ... INNER JOIN "Orders" AS "t1" ON ... AND ("t1"."Total" > ?)
This placement is required for _left_join — a row-level filter
in the outer WHERE would drop NULL-extended unmatched rows
and silently turn LEFT JOIN into INNER JOIN. _join (INNER)
uses the same ON placement for consistency and planner stability.
7.10.16.4. The into projection
The into lambda has two args bound to the two sources. Naming
is up to you (u, o here). Refer to columns from each source
through the arg name (u.Name, o.Total); the _sql
translator rewrites them to alias-qualified column refs.
Single-column and named-tuple projections both work. Per-source
projections (_select(...) on the right side) are rejected —
the join’s into lambda is the only projection.
7.10.16.5. Joining + grouping
_group_by / _having / _order_by after _join accept the
join’s into-projection alias names as keys, not base-table fields.
The translator resolves each alias through the join’s projection
registry and emits the underlying qualified column ("t0"."brand",
"t1"."Id") in the GROUP BY / HAVING / ORDER BY clauses, so the
SQL composes cleanly with the JOIN:
let groups <- _sql(db |> select_from(type<Car>)
|> _join(db |> select_from(type<Dealer>),
$(c : Car, d : Dealer) => c.dealer_id == d.id,
$(c : Car, d : Dealer) => (Brand = c.brand, Price = c.price))
|> _group_by(_.Brand)
|> _select((Brand = _._0, N = _._1 |> count())))
// SELECT ("t0"."brand"), COUNT(*)
// FROM "Cars" AS "t0" INNER JOIN "Dealers" AS "t1"
// ON "t0"."dealer_id" = "t1"."id"
// GROUP BY ("t0"."brand")
Per-group aggregates over a projection alias resolve the same way —
_._1 |> _select(_.Price) |> sum() becomes SUM("t0"."price"),
not the bare alias:
let totals <- _sql(db |> select_from(type<Car>)
|> _join(db |> select_from(type<Dealer>),
$(c : Car, d : Dealer) => c.dealer_id == d.id,
$(c : Car, d : Dealer) => (Brand = c.brand, Price = c.price))
|> _group_by(_.Brand)
|> _select((Brand = _._0,
Total = _._1 |> _select(_.Price) |> sum())))
// ... SUM("t0"."price") ... GROUP BY ("t0"."brand")
_order_by and computed-expression group keys (_group_by(_.Price / 100))
read aliases through the same registry. Aliases that don’t appear in
the join’s into projection reject with a clear macro_error listing
the valid alias names.
7.10.16.6. Other join shapes
_left_join / _right_join / _full_outer_join /
_cross_join all ship — see SQL-16 — All Join Shapes, which
covers every outer-join shape and the WHERE-on-preserved-side rules.
Three-table joins chain cleanly: each _join’s named into
projection is referenced by alias in the next join’s lambdas (e.g.
uo.OrderId). The one constraint is that each join’s RIGHT side
cannot itself be a join — chain from the left.
7.10.16.7. What’s not shipped
Multi-key equi-joins (
u.Id == o.UserId && u.Tenant == o.Tenant) — the backingjoinfn’s keys are single-column, so a&&-chainedONpredicate errors witherror[50503]; use raw SQL for composite-key joins.
See also
Full source: tutorials/sql/15-join.das
Previous tutorial: SQL-14 — _group_by and _having
Next tutorial: SQL-16 — All Join Shapes