ReactiveDag.Node.Join (reactive_dag v0.17.0-rc.58)

Copy Markdown View Source

A declarative JOIN: read ONE input's payload, index it into a LEFT and a RIGHT side (each a %{join_key => item} built from a per-side key fn), then emit one row per left key joined to its right item (right may be absent). The common declared-vs-observed reconcile/variance shape — the author writes the two side keys + the join row, not the read/write/changed plumbing.

LEFT join by default. outer: true makes it a FULL OUTER join: right-only keys also emit (into.(jk, nil, right_item)), for reconciles where an unexpected right-side member is itself a finding (a rogue repo the baseline never declared) rather than something to silently drop.

Two inputs

left_over:/right_over: read TWO DIFFERENT nodes, each scoped by its own keys. That needs left_owns:/right_owns: — the payload columns each side writes — because a claim names one side's keys and leaves the other side unread.

left_over:/right_over: read TWO DIFFERENT nodes. An earlier attempt at this shape was reverted for writing nils over good data — a budget claim destroyed the actual and vice versa — and the revert concluded that was "the shape's natural failure mode, not an oversight". The failure was real; the diagnosis was one level too shallow.

A claim key is a JOIN key. The reverted version scoped both sides by the one claim's keys against each side's own payload key — two different columns, usually different values (an Actuals row keyed "a1" joins on acct: "5000"). So the scoped read matched nothing, the side came back empty, and into emitted nil for its columns.

Each side is therefore scoped by the column it is INDEXED BY — its own join-key attribute. Both sides then read the rows the claim is actually about, into sees real values on both, and no nil is ever emitted to be written. Nothing about per-column writes is needed; the read was the bug.

Two consequences worth knowing:

  • A fn side computes its join key in the BEAM, so there is no column to push a filter into and that side reads WHOLE. Correct, and no worse than the one-input form, which also reads whole for a fn side.
  • The claim rule defaults to :group, not :identity: an input's changed keys are its own, so they are translated to join keys through the side that propagated — the same edge the read is scoped by.

So left/right/inner/outer stay ordinary options; outer: true is a full outer join here exactly as it is with one input.