///|
pub(all) enum JoinKind {
Inner
Left
} derive(Debug, Eq)
///|
pub impl Show for JoinKind with fn to_string(self) -> String {
match self {
Inner => "JOIN"
Left => "LEFT JOIN"
}
}
///|
/// One table joined onto a query, with the condition that attaches it.
pub struct Join {
kind : JoinKind
table_name : String
tbl : String
on : RawExpr
}
///|
/// Join another table, pairing the two tables' column handles.
///
/// `Cols` becomes a pair. Chaining nests to the left — `((C1, C2), C3)` — which
/// `Query::map_cols` collapses into names once the joins are done.
pub fn[C1, C2, R2, A] Query::join(
self : Query[C1, A],
t : Table[C2, R2],
on : (C1, C2) -> Expr[Bool],
) -> Query[(C1, C2), A] {
attach(self, t, on(self.cols, t.cols).raw(), (self.cols, t.cols), Inner)
}
///|
/// Assemble the joined query. Only the join list and the column handles change.
fn[C1, C2, R2, A, D] attach(
q : Query[C1, A],
t : Table[C2, R2],
on : RawExpr,
cols : D,
kind : JoinKind,
) -> Query[D, A] {
let joins = q.joins.copy()
joins.push({ kind, table_name: t.table_name, tbl: t.tbl, on })
let renamed = q.respec(
cols~,
projection=q.projection,
group=q.group,
decode=q.decode,
)
{ ..renamed, joins, }
}
///|
/// The right-hand side of a LEFT JOIN.
///
/// Wrapping the joined table's column handles rather than exposing them keeps
/// the nullability from being lost: there is no way to reach a `Column[T]` of
/// an outer-joined table, only a `Column[T?]` through `col`, or the whole row
/// as `Selection[R?]` through `row`.
struct Nullable[C, R](C, Selection[R])
///|
/// Take one column of the outer-joined table, as a nullable column.
pub fn[C, R, T] Nullable::col(
self : Nullable[C, R],
f : (C) -> Column[T],
) -> Column[T?] {
let Nullable(cols, _) = self
f(cols).nullable()
}
///|
/// Take the whole outer-joined row, absent when nothing matched.
///
/// This is usually what a LEFT JOIN means: not that each column independently
/// might be NULL, but that the row on the right either exists or does not.
/// Inside `Some`, every field keeps the type the table declared.
pub fn[C, R] Nullable::row(self : Nullable[C, R]) -> Selection[R?] {
let Nullable(_, all) = self
all.optional()
}
///|
/// Outer-join another table, pairing the column handles.
/// `on` sees the joined table's raw columns, because a join condition is
/// evaluated before the outer-join padding and nullability does not apply to
/// it. What the query carries afterwards is the `Nullable` wrapper.
pub fn[C1, C2, R2, A] Query::left_join(
self : Query[C1, A],
t : Table[C2, R2],
on : (C1, C2) -> Expr[Bool],
) -> Query[(C1, Nullable[C2, R2]), A] {
attach(
self,
t,
on(self.cols, t.cols).raw(),
(self.cols, Nullable(t.cols, t.all)),
Left,
)
}
// Destructuring the joined columns.
//
// Chained joins nest their column handles to the left, and MoonBit has no
// pattern in a lambda parameter to take them apart with. These adapters do it
// instead: they turn a function of N named arguments into the single-argument
// function the combinators expect, so `.0.0` lives here and not in user code.
///|
/// Take a two-table join apart into its two column sets.
///
/// |> Query::filter(split2((u, p) => u.age.gte(18) & p.title.ne("draft")))
///
/// Deliberately stops at two. With three tables the arguments start needing
/// positional `_` placeholders, at which point naming the shape with
/// `Query::map_cols` reads better than destructuring it.
pub fn[A, B, R] split2(f : (A, B) -> R) -> ((A, B)) -> R {
t => f(t.0, t.1)
}