ddn.df.join

The join layer of ddn.df: inner and left joins emitting rows in left-frame order (T28; D12; ADR-2026-10-18-05).

The table is built on the right key (the smaller probing side) and probed with the left key in sequence, so both join kinds emit rows in left-frame order and, within one left row, in right-frame row order — a stronger contract than Polars' maintain_order='none', which lets the cross-chunking harness compare join results exactly. Null keys never match; unmatched retention follows the kind (ADR-2026-10-23-07: LEFT and ANTI retain null-key left rows, INNER and SEMI drop them); the key column is coalesced into one output column; right-side name collisions get the suffix.

Types 6

The join kinds (ADR-2026-10-23-07). Null keys never match in any kind; retention follows the kind: INNER drops unmatched (and null-key) left rows, LEFT retains every left row (null-key rows null-padded - the Polars semantics, adopted with explicit compatibility approval), SEMI keeps only matched left rows, ANTI only unmatched ones (null-key left rows included).

INNEROnly rows with a match on both sides.
LEFTAll left rows; unmatched rows (null keys included) get null right-side columns.
SEMIThe matched left rows, left schema, no duplication.
ANTIThe unmatched left rows (null keys included), left schema.
OUTERFull outer: every matched pair in left order, every unmatched left row null-padded on the right, then every unmatched right row (right-row order) null-padded on the left; the key is coalesced.
private structJoinEntry
Fields
size_t hash
size_t row
size_t next
private structChainTables

The small-right serial chain table (the original build, unchanged).

Fields
const(JoinEntry)[] entries
const(bool)[] used
private structCsrTable

The large-right slot-partitioned CSR table (ADR-2026-10-23-03): every right row appears exactly once in hashes/rows, its slot's region [starts[slot] .. starts[slot + 1]] holding it in right-row order - the same set in the same order as the chain table's walk, with no pointers to chase and no way to cycle. Built by three deterministic phases with one writer per precomputed position.

Fields
size_t[] starts
size_t[] hashes
size_t[] rows

The asof match direction (ADR-2026-10-30-03).

BACKWARDThe last right row with `rightKey <= leftKey`.
FORWARDThe first right row with `rightKey >= leftKey`.
NEARESTThe smaller distance; exact ties break toward BACKWARD.
private structAsofKeys
Fields
ArrayData[] chunks
size_t[] starts
Kind kind
Methods
bool isNull(size_t row) @safe pure nothrow
long asLong(size_t row) @safe pure nothrow
double asDouble(size_t row) @safe pure nothrow

Functions 17

private fnCsrTable buildCsrTable(const size_t[] rHashes, size_t rheight, size_t capacity)Builds the CSR table (ADR-2026-10-23-03). Phase A scatters the entries into 64 hash-high-bit partitions (per-segment histograms, a partition-major/segment-minor serial prefix - the `argSortRadix` d...
private fnvoid probeSegments(bool csr, Tables)(Tables tables, size_t mask, const ArrayData[][] lkeyChunks, const size_t[] lstarts, const size_t[] lHashes, const ArrayData[][] rkeyChunks, const size_t[] rstarts, const ColumnEq[] eqs, JoinKind kind, size_t segLen, size_t nSegs, size_t lheight, ref size_t[][] segLeft, ref size_t[][] segRight, size_t[][] matchedRight = null)The probe (ADR-2026-10-19-02, ADR-2026-10-23-03): fixed left-row segments over the immutable table, each first counting its hash-equal candidates (integer compares only - the typed key equality run...
private fnsize_t chunkIndexOf(const size_t[] starts, size_t row) @safe pure nothrowJoins two frames on equal-dtype key columns. The output columns are the left frame's followed by the right frame's minus the key; the key is coalesced into one column; right-side name collisions ge...
private fnbool crossRowCount(size_t lheight, size_t rheight, out size_t count) @safe pure nothrowThe checked Cartesian row count (ADR-2026-10-30-03): the product must fit `size_t`; a null result rejects before any allocation.
fnDataFrame crossJoinFrames(DataFrame left, DataFrame right, string suffix = "_right")The cross join (ADR-2026-10-30-03): the left-major nested loop - every left row in order, all right rows in right order within it - with the product's row count overflow-checked before any allocati...
fnDataFrame joinFrames(DataFrame left, DataFrame right, string leftOn, string rightOn, JoinKind kind = JoinKind.INNER, string suffix = "_right")
fnDataFrame joinKeys(DataFrame left, DataFrame right, string[] leftOn, string[] rightOn, JoinKind kind = JoinKind.INNER, string suffix = "_right")The multi-column join (ADR-2026-10-30-03): a row matches only when every key pair holds under the existing per-kind rules; any null component never matches (LEFT keeps the row null-padded, SEMI dro...
private fnsize_t[] chunkStartsOf(const ArrayData[] chunks) @safe pure nothrowThe chunk-start offsets of a chunk list.
private fnsize_t[] concatSegments(size_t[][] segs, size_t nSegs)Key-row equality across the two sides' chunkings: nulls never match, values compare through the preselected typed equality at the key dtype, and both rows locate by binary search over the chunk sta...
private fnvoid probeExistence(bool csr, Tables)(Tables tables, size_t mask, const ArrayData[][] lkeyChunks, const size_t[] lstarts, const size_t[] lHashes, const ArrayData[][] rkeyChunks, const size_t[] rstarts, const ColumnEq[] eqs, JoinKind kind, size_t segLen, size_t nSegs, size_t lheight, size_t[][] segKeep)The existence probe of SEMI/ANTI (ADR-2026-10-23-07): fixed left-row segments over the immutable table, each filling its kept left rows - SEMI keeps rows with at least one match, ANTI the rest (nul...
private fnbool rowsEqual(const ArrayData[][] lcols, const size_t[] lstarts, size_t lrow, const ArrayData[][] rcols, const size_t[] rstarts, size_t rrow, const ColumnEq[] eqs) @safe pure nothrow
private fnbool rowsEqualAt(const ArrayData[][] lcols, size_t lc, size_t li, const ArrayData[][] rcols, const size_t[] rstarts, size_t rrow, const ColumnEq[] eqs) @safe pure nothrow
private fnsize_t[] combinedRowHashes(Series[] keys) @safe
private fnsize_t chunkIndexFor(const size_t[] starts, size_t row) @safe pure nothrowThe binary search of the chunk holding a global row.
private fnT readAs(T, Kind K)(Series col, size_t i)
fnDataFrame joinAsofFrames(DataFrame left, DataFrame right, string leftOn, string rightOn, AsofDirection direction = AsofDirection.BACKWARD, long tolerance = long.max)The sorted asof join (ADR-2026-10-30-03): for each left row (in order, at most one right row), BACKWARD takes the last right row with `rightKey <= leftKey`, FORWARD the first with `>=`, NEAREST the...
private fnsize_t totalLen(AsofKeys keys) @safe pure nothrow