///|
/// How an equi-join combines rows that share (or fail to share) a key.
///
/// `pub(all)` so callers can name the variants directly (e.g.
/// `JoinOptions::on(keys, how=Left)`), matching `@types.SortOrder` / `@types.NullOrder`:
/// * `Inner` — keep only rows whose key matches on both sides.
/// * `Left` — keep every left row; where it has no right match, the
/// right-hand columns are filled with nulls.
/// * `Right` — the mirror of `Left`: keep every right row; where it has
/// no left match, the left-hand columns are filled with nulls. The
/// output still leads with the left columns (only the row set changes
/// to "all right rows").
/// * `Outer` — the full outer join: keep matched pairs plus every
/// unmatched row from **both** sides (the missing side's columns null).
/// Left rows (matched and unmatched) come first in left order, then the
/// unmatched right rows in right order.
/// * `Cross` — the Cartesian product (every left row paired with every
/// right row); takes **no** key columns.
pub(all) enum JoinType {
Inner
Left
Right
Outer
Cross
} derive(Eq, Debug)
///|
pub extend JoinType with Eq::{equal, not_equal}
///|
pub extend JoinType with Debug::{to_repr}
///|
/// The knobs for `DataFrame::join`: which columns to match on, the join
/// `how`, the `suffix` disambiguating a right column whose name collides
/// with a left one, and whether to `coalesce` the key columns. Fields are
/// read-only outside the package — build options through `JoinOptions::on` /
/// `JoinOptions::left_on` / `JoinOptions::cross`, naming only the knobs that
/// differ from their defaults. The three entry points are the legal shapes: an
/// `on` join, a paired `left_on` / `right_on` join, and a keyless cross join —
/// no combination of them can be spelled.
///
/// * `on` — the key **expressions**, evaluated over **both** frames
/// (Polars' `IntoExpr`). A bare `col("id")` matches on an existing
/// column; a derived key such as `col("ts") / lit_int(86400)` matches
/// on the computed value. `["region", "product"]`-style multi-key joins
/// become `[col("region"), col("product")]`, matched on the tuple. An
/// empty key list is **invalid** for `on` — a non-`Cross` join requires at
/// least one key (`DataFrame::join` raises `InvalidOperation`); use
/// `JoinOptions::cross()` for a Cartesian product.
/// * `left_on` / `right_on` — paired key expressions evaluated on the
/// left and right frame respectively, for joining on differently-named
/// or differently-derived keys (`left_on([col("a")])` against
/// `right_on([col("b")])`). Mutually exclusive with `on`, and the two
/// lists must name the same number of keys; key `i` on the left is
/// matched against key `i` on the right.
/// * `how` — `Inner` (the `on` / `left_on` default), `Left`, `Right`,
/// `Outer`, or `Cross`.
/// * `suffix` — appended to a **right** column whose name also occurs in
/// the left frame (default `"_right"`); the left column keeps its name.
/// * `coalesce` — whether each key column is merged into a single output
/// column. `None` (the default) auto-selects by `how`, matching Polars:
/// an inner / left / right join **coalesces** (the key appears once),
/// while `Outer` does **not** (the right key is kept as
/// ``, null wherever its row had no match, so matched and
/// unmatched rows are distinguishable). `Some(true)` always coalesces;
/// `Some(false)` never does. Coalescing only applies when keying by
/// `on` whose every key is a bare `col(...)` (the precondition for the
/// key to name a single column on both sides); `left_on` / `right_on`
/// and any derived key turn it off, keeping both key columns (Polars'
/// "join on a non-column expression turns off coalescing"). When
/// coalesced the single key takes its value from whichever side is
/// present on that row (the two are equal on a matched pair) — the left
/// on `Inner` / `Left`, the right on `Right`, and the present side per
/// row on `Outer`.
/// The three key lists are `priv`, read through the copying `on_keys()` /
/// `left_keys()` / `right_keys()`. A public `Array` field is readable, and
/// reading an array hands over the array *itself* — enough to push a key into
/// an options value a `LazyFrame::join` already captured, changing what a
/// built plan collects. The constructors copy on the way in for the same
/// reason; these accessors close the way out. The remaining fields are
/// immutable values, so they stay public.
pub struct JoinOptions {
priv on : Array[@expr.Expr]
priv left_on : Array[@expr.Expr]
priv right_on : Array[@expr.Expr]
how : JoinType
suffix : String
coalesce : Bool?
} derive(Debug)
///|
/// The shared key expressions (`JoinOptions::on`), or an empty array for a
/// sided or cross join. A copy: mutating it cannot alter the options, or any
/// plan holding them.
pub fn JoinOptions::on_keys(self : JoinOptions) -> Array[@expr.Expr] {
self.on.copy()
}
///|
/// The left-side key expressions (`JoinOptions::left_on`), or an empty array
/// when the join keys by `on` or is a cross join. A copy, like `on_keys`.
pub fn JoinOptions::left_keys(self : JoinOptions) -> Array[@expr.Expr] {
self.left_on.copy()
}
///|
/// The right-side key expressions paired with `left_keys`, or an empty array
/// when the join keys by `on` or is a cross join. A copy, like `on_keys`.
pub fn JoinOptions::right_keys(self : JoinOptions) -> Array[@expr.Expr] {
self.right_on.copy()
}
///|
pub extend JoinOptions with Debug::{to_repr}
///|
/// Start a join on shared key columns — each key expression is evaluated on
/// **both** frames (`JoinOptions::on([col("id")])`). `how` defaults to an
/// inner join, `suffix` to `"_right"` for a right column whose name collides
/// with a left one, and an omitted `coalesce` leaves key coalescing on
/// automatic (by `how`, matching Polars); pass `coalesce=true` / `false` to
/// force it.
///
/// The `keys` array is copied (as in `left_on`), so mutating it after
/// construction cannot alter the join specification.
pub fn JoinOptions::on(
keys : Array[@expr.Expr],
how? : JoinType = Inner,
suffix? : String = "_right",
coalesce? : Bool,
) -> JoinOptions {
{ on: keys.copy(), left_on: [], right_on: [], how, suffix, coalesce }
}
///|
/// Start a join on differently-named (or differently-derived) keys: `keys` is
/// evaluated on the **left** frame, paired position-by-position with
/// `right_on` on the right
/// (`JoinOptions::left_on([col("a")], right_on=[col("b")])`). Taking both
/// sides at once makes an unpaired specification unspellable. Defaults match
/// `on`, except that `left_on` / `right_on` keys never coalesce — both key
/// columns are kept (Polars' rule).
///
/// Both arrays are copied, so mutating them after construction cannot alter
/// the join specification.
pub fn JoinOptions::left_on(
keys : Array[@expr.Expr],
right_on~ : Array[@expr.Expr],
how? : JoinType = Inner,
suffix? : String = "_right",
coalesce? : Bool,
) -> JoinOptions {
{
on: [],
left_on: keys.copy(),
right_on: right_on.copy(),
how,
suffix,
coalesce,
}
}
///|
/// Start a cross-join specification — the Cartesian product of the two
/// frames, with no key columns (`how = Cross`, all key lists empty).
/// `suffix` still applies to a right column whose name clashes with a left
/// one; `coalesce` is irrelevant (there are no keys to merge).
pub fn JoinOptions::cross(suffix? : String = "_right") -> JoinOptions {
{ on: [], left_on: [], right_on: [], how: Cross, suffix, coalesce: None }
}