Repository navigation
JOIN TRUNCATE of either side, image-less changes and quarantine release recompute the JOIN's cells instead of halting (J5a, part of #1110) - #1163
Merged
mmmries merged 2 commits intoOct 19, 2026
Conversation
mmmries
force-pushed
the
feat/issue-1110-join-truncate-recompute
branch
from
October 19, 2026 03:53
52b5b33 to
c95db12
Compare
…ked writer (J5a, part of salesforce-misc#1110) A JOIN behind the group-basis knob used to halt on a TRUNCATE of either side, and on any image-less change: a released key, the automatic retry, or a Re-derive. Both now apply, as ADR-0019 and ADR-0002 require. TRUNCATE: the truncate's page takes the pair's lock row FOR UPDATE (every other writer takes it FOR SHARE). For `a` it empties P and T; for `b` it empties M, zeroes every J.matches, empties T and refills T from P (P ⊗ U for LEFT). Each side raises its own floor, the visible target is cleared through the seam, and J rows are kept. The hidden tables are emptied with DELETE: a waiting release holds ACCESS SHARE on them through its working tables' CTAS, so a TRUNCATE would deadlock against it. The global truncate drain barrier already gives ADR-0019's two-sided barrier, mixed pages included. Cell recompute: a JOIN side's plan now names the cells to recompute (the key's current row's and its prior-image hint's) and rewrites them with ADR-0002's recompute statement under the lock row, the J locks and the cell locks. Each cell's change goes into the side's deltas, and the product step sums those per cell and carries them into T. Parking a JOIN key keeps its side's cells in agg_groups, and release_key/retry_held_key recompute them in the release transaction. Tests port J1 family 10 (truncate races at every pause point, stale pages, both sides, b with every key matched) and the mixed-page barrier to the engine, plus image-less recompute, release, retry, and a release racing a page of the other side on its J lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and the retry's own recompute (salesforce-misc#1110) The automatic-retry test checked the targets only after settling, so the Recompute the release stages repaired them even with join::release a no-op; it now checks them before anything drains, as the release test does. Two tests the change lacked: - a page that applies captured changes to a cell and recomputes it, on both sides (a hidden flip included): the cell has two delta rows, and taking either one instead of their sum fails it; - a TRUNCATE of the joined side refilling unmatched groups whose joined-side GROUP BY columns are timestamptz and numeric(10,2), on a text/integer pair key, with a NULL-key fact; skipping the refill fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mmmries
force-pushed
the
feat/issue-1110-join-truncate-recompute
branch
from
October 19, 2026 04:55
c95db12 to
e921ae9
Compare
mmmries
added a commit
that referenced
this pull request
Oct 19, 2026
…enerative tier draws truncates (J5b, closes #1110 with #1163) (#1171) * JOIN schema change, retyping and capture audits on both tables (J5b, part of #1110) A JOIN behind the group-basis knob already paused on a lost, renamed or re-keyed column, row-level security, a subscription or a hierarchy on either table (#1107's readers_of). Four paths still saw only the FROM table, or nothing: - defs::copies recorded no copies for a JOIN, so a widened column on either table was never followed. A catalog-only varchar widening left the target and P/M/J/T narrow, so every longer value failed its page. A rewriting widening under SUM() never paused. join_copies now records the target's GROUP BY columns, the hidden tables' side-typed columns (exact type and collation) and the field columns. Retype-in-place, the retype releases (through J5a's join::release), the outgrown pause and the resume's re-type all apply to them. copies::inputs fingerprints the joined table and the hidden tables for the drift memo. - capture::baseline's evaluated-as check compared only the FROM table's recorded types, so the joined table's recorded join_columns went stale silently. It now checks both. - self_check's capture audit listed only the source and relationship to-sides. It now audits the joined table too. - Column quarantine needs no change: the column fuse is 1-1-only, and a JOIN is treated as an aggregate is. The generative JOIN tier now draws TRUNCATE on either table, in half the cases. Docs list the JOIN's copies, and gap 23 covers them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: a JOIN resume recreates its hidden tables instead of re-typing them first (#1110) J5b made a JOIN's hidden-table columns typed copies, so a resume asked the staging worker to ALTER P, M, J and T before complete_resume drops and recreates them anyway. J keeps every key it has seen, so after an ON pair narrows (bigint to integer) a key whose rows are gone fails the ALTER, and the resume ended with "DROP TRANSFORM and define it again" where recreating J succeeds. It also rewrote P (up to the fact table's size) under ACCESS EXCLUSIVE just to drop it. Both resume paths now leave JoinHidden copies to the recreate. Gap 16's and gap 23's self_check lines now count the JOIN audit J7a (#1166) landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
J5a of #1110: a
JOINtransform (behindTRELLIS_DEV_GROUP_BASIS=1) now handles aTRUNCATEof either side and recomputes cells, where before it halted withGroupBasisUnsupported. The cell recompute covers image-less changes, quarantine releases and the automatic retry. Column quarantine, schema change and retyping, and the row-security and capture audits are J5b and are not in this PR.Design
Per-side
TRUNCATE(ADR-0019, "TRUNCATEof either side excludes writers of both"). The truncate's page takes the pair's lock rowFOR UPDATE; every other writer takes itFOR SHARE. The page then clears what the truncate empties before it applies its other changes:TRUNCATE a: emptiesPandT.TRUNCATE b: emptiesM, sets everyJ.matchesto 0, emptiesT, then refillsT's staging fromP(P ⊗ UforLEFT, nothing forINNER). The refill readsPbefore the page's own sides apply, so it composes with them.Each side raises its own truncate floor, keyed by its hidden table, so a change from before the truncate is refused. The visible target is cleared through the seam with
clear_target.Jrows are kept.The hidden tables are emptied with
DELETE, notTRUNCATE. A release isn't a page, so the drain barrier doesn't keep it out: it can run alongside a truncate page. Before it reaches the lock row, it already holdsACCESS SHAREon the hidden tables from its working tables'create … as select … with no data. Parse analysis takes that lock even when the temp table already exists. ATRUNCATE'sACCESS EXCLUSIVEwould then deadlock against it. The cost is that aTRUNCATEof a side deletesP(orM) andTrow by row: WAL per row, and dead tuples left for autovacuum. That's the same cost the visible target's clear already pays. It's proportional to the cell count, not the source's row count, but on a shape whose cells are near the fact table's size, one truncate page is large.Two-sided barrier. The engine's drain barrier is already stronger than ADR-0019 requires. A truncate-bearing segment drains alone, after every earlier segment of any table and before every later one, and is never coalesced. That covers mixed pages, and no new mechanism was needed.
family10_a_mixed_page_waits_for_an_earlier_truncatepins it.Cell recompute. A
JOINside'sGroupTargetPlannow has a table of the cells to recompute and its source's row identity. An image-less change names its key's current row's cell and its prior-image hint's cell; forM, cells with aNULLkey part are filtered out. These cells go through the J-locked writer, in this order:Jinserted for the named keys, thenJlocked sorted.TandJ.matches(JR1–JR3).Release and automatic retry. Parking a JOIN key records the cells of its side in
agg_groups.release_key(and soretry_held_key) callsjoin::release, which recomputes those cells and the key's current row's cell in the release transaction, using the same steps.No migration.
Verification
New tests in
trellis/tests/join_interleavings.rsare deterministic and don't poll:TRUNCATE a/bvs. a page of the other side.LEFT,INNERand the pair key.TRUNCATE bwith every key matched leaves allJ.matchesat 0, keeps everyJrow, empties theINNERtargets and leaves onlyNULL-gbgroups inLEFT.S10.mixed-page-barrier): the mixed batch can't be claimed until the earlier truncate's batch drains.TRUNCATEof the joined side refills unmatched groups whose joined-sideGROUP BYcolumns aretimestamptzandnumeric(10,2), on atext/integerpair key, with aNULL-key fact. The refill's barenulls take the target columns' types fromINSERT … SELECT.Recomputeit stages. The automatic-retry test checks the same way.Jlock on.Mutation checks, each run against these tests and then reverted:
join::releasea no-op fails the 3 release and retry tests.FOR SHAREon a truncate page fails the family 10 races: a page of the other side ran during the frozen truncate and leftT≠Σ P ⊗ M.LEFTtruncate tests and the typed-refill test.join_interleavings.rstakes about 70 s (40 tests).verify: green, passed=3593 failed=0 ignored=40 (tree 09593a3d50, rebased on main e3e08e4)Part of #1110
🤖 Generated with Claude Code