You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: backlog/tasks/task-59 - Stage-B-join-multiplicity-under-shapemany-—-dup-key-cross-inequality-and-self-joins.md
+18-8Lines changed: 18 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,10 +3,10 @@ id: TASK-59
3
3
title: >-
4
4
Stage B: join multiplicity under shape='many' — dup-key, cross/inequality, and
5
5
self-joins
6
-
status: In Progress
6
+
status: Done
7
7
assignee: []
8
8
created_date: '2026-07-28 01:55'
9
-
updated_date: '2026-07-28 01:57'
9
+
updated_date: '2026-07-28 03:07'
10
10
labels:
11
11
- specializer
12
12
- stage-b
@@ -29,12 +29,12 @@ Hard stop after shipping: columnar-API discussion with the user before any next
29
29
30
30
## Acceptance Criteria
31
31
<!-- AC:BEGIN -->
32
-
-[]#1 Pins spec for multiplicity semantics (emission order/determinism, LEFT-miss, cross/inequality, self-join star forms, NULL keys among dups) with raw JSONs
33
-
-[]#2 All stage-B constructs REJECT under shape='filter'/'map' exactly as today; they build only under shape='many'
34
-
-[]#3 1:N equi-joins, cross/inequality joins, and self-joins serve bit-exact (order-insensitive multiset vs DuckDB) under shape='many'
35
-
-[]#4 Corpus replay strictly above 529 with zero FAILs (replay builds stage-B cases with shape='many')
36
-
-[]#5 Limitations doc + twin updated: stage-B rows move from 'designed, not built' to the shape='many' contract
37
-
-[]#6 Full gates green on release build; PR opened (stacked on #44)
32
+
-[x]#1 Pins spec for multiplicity semantics (emission order/determinism, LEFT-miss, cross/inequality, self-join star forms, NULL keys among dups) with raw JSONs
33
+
-[x]#2 All stage-B constructs REJECT under shape='filter'/'map' exactly as today; they build only under shape='many'
34
+
-[x]#3 1:N equi-joins, cross/inequality joins, and self-joins serve bit-exact (order-insensitive multiset vs DuckDB) under shape='many'
35
+
-[x]#4 Corpus replay strictly above 529 with zero FAILs (replay builds stage-B cases with shape='many')
36
+
-[x]#5 Limitations doc + twin updated: stage-B rows move from 'designed, not built' to the shape='many' contract
37
+
-[x]#6 Full gates green on release build; PR opened (stacked on #44)
38
38
<!-- AC:END -->
39
39
40
40
## Implementation Plan
@@ -46,3 +46,13 @@ TODAY: both backends are strictly one-emit-per-row. Interpreter: per row, block
46
46
47
47
DESIGN DIRECTION for multiplicity (validate in spec): (1) emission becomes an explicit operation instead of a terminal side effect — either Inst::EmitRow (appends the current out-lane values) with CTerm::Emit demoted to plain end-of-row, or loop-structured blocks using the EXISTING Brif/Jump machinery (loop header + body + back-edge) with an EmitRow inst in the body. Verify/canonicalize must accept back-edges (check: today's block graph is probably a DAG — verify.rs may assume forward-only; if so that is the first thing to extend). (2) New probe ops: ProbeStart {join} -> cursor, ProbeNext {join, cursor} -> (has: i1, cursor', value lanes) over per-key row LISTS in StaticTy::Map (values become Vec-of-rows per key — flat arena + (off, len) per key is the lean layout). (3) Cranelift: emission via a helper call h_emit_row(cx) that appends to st.out (helpers already write through Cx), loop via normal CLIF blocks — feasible without new architecture; the return code stays for trap/end only. If this turns out heavy, interpreter-first for shape='many' with cranelift fallback is the documented fallback plan (bench guard exempts 'many' from backend-identity tests). (4) Self-join: per-call build of the batch-side map before the row loop (new PreparedStatic variant built in run(), or a batch-map handed via RunState); marshaller unaffected. (5) Cross/inequality: a join with ZERO key columns = single bucket = ProbeStart/Next over the whole static (or batch) + residual predicate in the loop body. (6) LEFT: emit null-extension when the loop body emitted nothing for this row (a per-row 'any_emitted' flag register). (7) shape gate: prepare learns the target shape (prepare_opaque param or Prepared metadata + duckdb/mod.rs check): multiplicity constructs produce a NAMED reject unless shape='many'; corpus replay builds stage-B-shaped cases with shape='many' (detect by retrying on the named errors, or always pass shape='many'? NO — corpus must keep proving the default rejects; retry-with-many on the named needles is the honest harness).
48
48
<!-- SECTION:PLAN:END -->
49
+
50
+
## Final Summary
51
+
52
+
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
53
+
Stage B shipped as two stacked PRs: #45 (part 1: dup-key equi-joins, cross/inequality/constant-ON joins) and #46 (part 2: self-joins via the batch as build side). Corpus 529 -> 550 match / 0 FAIL of 678 (81%); the 27-case stage-B pool is fully resolved (21 served + 3 USING-self-join named rejections + 3 rowid self-joins routed to the documented rowid descope).
54
+
55
+
Everything builds ONLY under shape='many' (TASK-58's fence): default shapes are byte-identical, dup keys and self-joins still reject. Machinery: Term::EmitTo emit-and-continue back-edges (verifier now allows terminating cycles only), StaticTy::MultiMap (stable-sorted equal-key runs; insertion order = the engine's documented 1:N emission order) + ProbeRange/ProbeRead, StaticTy::BatchMap (per-call batch flattening for self-joins, whole ON as per-pair residual — cross-then-filter, pins-proved identical), loop-structured lowering riding state on the live stack with per-block probe-cache reseeds (block splits from CASE machinery), residual-vs-WHERE gate split (residual decides match-ness and the LEFT null-extension; WHERE only gates emission). Interpreter-only; cranelift pre-rejects into the existing fallback.
56
+
57
+
Central pins 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 (corpus sorts; duck oracle tests sort), and the engine's own deterministic order (probe outer, insertion inner, null-extension in place) is pinned as a test. Pins: 2026-07-28-stageB-multiplicity-pins.md + pins-stageB/*.json (5 agents, 57 pins). Follow-up left open: USING/NATURAL self-joins (named rejection).
0 commit comments