Skip to content

Stage B part 1: join multipliplicity under shape='many' — corpus 529 -> 546 (TASK-59) - #45

Merged
claude-agent-ahrzb[bot] merged 7 commits into
masterfrom
task-59-stage-b
Jul 28, 2026
Merged

Stage B part 1: join multipliplicity under shape='many' — corpus 529 -> 546 (TASK-59)#45
claude-agent-ahrzb[bot] merged 7 commits into
masterfrom
task-59-stage-b

Conversation

@claude-agent-ahrzb

Copy link
Copy Markdown

Join multiplicity, behind the shape fence

Every construct here builds ONLY under the explicit shape='many' opt-in (PR #44's fence) — the default shapes are byte-identical to before, dup keys still reject.

What serves now (pins: 2026-07-28-stageB-multiplicity-pins.md, 57 pins / 5 agents)

  • 1:N equi-joins against statics with duplicate keys — full per-key cross products, LEFT null-extension per zero-match row (including the all-matches-residual-filtered case), NULL keys match nothing.
  • Cross joins, inequality ON, constant ON — the keyless one-bucket range (wave-4's empty-key binding meant this was nearly free).
  • Residual-vs-WHERE split done right: the residual decides match-ness (and the null-extension flag); WHERE only gates emission — a match killed by WHERE still suppresses the LEFT null-extension, per DuckDB's join-then-filter order.

The order finding (fleet-measured)

DuckDB's join output order is a hash-join accident — LIFO chains in lockstep vector passes, flips with table size or projection, non-deterministic run-to-run at 200k+ rows with threads. So parity is multiset (sorted compare; the corpus already sorts), and the engine ships its own documented deterministic order: probe rows in input order, matches contiguous in build insertion order.

Machinery

Term::EmitTo (emit-and-continue back-edges; the verifier now allows terminating cycles only), StaticTy::MultiMap + ProbeRange/ProbeRead (stable-sorted equal-key runs), loop-structured lowering riding state on the live stack across block splits. Interpreter-only: cranelift pre-rejects multiplicity programs into the existing fallback.

Not in this PR

Self-joins (10 corpus cases) — the batch-as-build-side piece, next PR on this ticket. The left_join_issue_1172 family turned out to be pure NULL-semantics (measured), so it landed here.

Gate

Corpus 546 / 132 / 0 FAIL of 678 (was 529). cargo 167, pytest 616 + 13 xfail. Also carried: the columnar-path decision doc + measured us-vs-DuckDB-on-arrow numbers.

🤖 Generated with Claude Code

ahrzb and others added 7 commits July 28, 2026 05:13
…ge-B discussion)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, engine-defined order (TASK-59)

Five-agent fleet, 57 pins. Row-SET contract: per-pair emission with full
per-key cross products; LEFT = max(1, surviving matches) incl. the
all-matches-filtered null-extension; NULL keys match nothing (even <>);
keyless joins are cross-then-filter; USING carries the LEFT value on
misses. Central finding: DuckDB's join output ORDER is a hash-join
accident (LIFO chains, lockstep vector passes, run-to-run divergence at
scale) — parity is MULTISET, and the engine defines its own documented
deterministic order. The 1172 family turns out to be pure NULL-semantics
(no fan-out at all).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…me ours by 1-3 orders, crossover ~2-3k rows/call today

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tics, range probes (TASK-59 stage B-1)

- Term::EmitTo { to, args }: emit the completed row AND continue at a
  block — the multiplicity loop's back-edge. Verifier: cycles are now
  legal iff every block still reaches a row-ENDING terminator
  (emit/skip/trap), which keeps rejecting degenerate spin loops; the
  store dataflow excludes back-edges from its topo order and EmitTo
  propagates the RESET all-zero store state to the loop header.
- StaticTy::MultiMap + Inst::ProbeRange/ProbeRead: duplicate-key maps
  probed as a flat equal-key row range (stable sort keeps INSERTION
  order among equal keys — the engine's documented 1:N emission order);
  zero keys = the whole table (keyless cross/inequality joins).
- Interpreter executes all of it; cranelift pre-rejects multiplicity
  programs so the existing interp fallback is the documented 'many'
  path. Print/parse roundtrip (multimap header, probe.range/probe.read,
  emit.to) + two machinery fixtures: dup-key fan-out with skip-on-miss,
  and the keyless cross join.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nder shape='many' (TASK-59 stage B-2)

lower() gains many: bool (one join per query in stage B, named
restriction). The join lowers as a multiplicity loop: entry evaluates
keys (NULL keys force an empty range) + ProbeRange; header/body iterate
the equal-key run with ProbeRead lanes seeded into the block probe
cache; the RESIDUAL gates match-ness (and the LEFT null-extension flag)
while WHERE only gates emission — a match killed by WHERE still
suppresses the null-extension, per DuckDB's join-then-filter order; the
back-edge is emit.to. LEFT emits one null-extended row when nothing
matched, itself WHERE-gated with default lanes.

The multimap static declaration allows duplicate keys at
materialization ONLY on this path; the default shapes keep the 1:1 map
contract byte-identical (dup keys still error). E2E interp tests cover
fan-out order, INNER drops, residual-filters-all, WHERE composition,
and both named restrictions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…retry — 529 -> 546, 0 FAIL (TASK-59 stage B-3)

Predicate/expression emission may SPLIT blocks (CASE machinery), so the
multiplicity loop now rides its state and probe lanes on the LIVE stack
(auto-rebound across splits; lanes are always the trailing entries) and
re-seeds the per-block probe cache before every emission that can
reference join columns — fixes the three corpus binder failures. The
keyless path needed nothing: wave-4's empty-key binding already covers
cross joins, inequality ON, and constant ON under 'many' (test added).
Corpus replay retries 'duplicate map key' rejections with shape='many'
(the default's rejection stays proven); fn.shape now reports 'many'.

Corpus: 546 match / 132 clean-unsupported / 0 FAIL of 678 (was 529 —
the full dup-key + cross/inequality bucket). Self-joins are B-4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns doc/twin, corpus 546 (TASK-59 stage B-5)

- tests/test_duckdb_stageb_many.py: sorted-multiset oracle tests (dup-key
  fan-out, residual/WHERE combos, cross + inequality + constant ON) plus
  the engine's OWN order contract pinned as a test.
- known-limitations §1/§2: dup-key/cross/inequality joins now documented
  as serving under the shape='many' opt-in (multiset parity, engine-
  defined order); self-join row marked in-progress; corpus count 546.
- Twin + shape tests flipped: dup keys serve under many, reject under
  the defaults; fn.shape == 'many'.

Full gate: cargo 167, pytest 616 + 13 xfail, corpus 546/0 FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude-agent-ahrzb
claude-agent-ahrzb Bot merged commit 4f94d80 into master Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant