Repository navigation
fix: four core access-method bugs the missing-test audit found, each red first (#1341) - #1364
Conversation
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving 071ddec. The strongest proof I could run: this PR's tests on main's product code. I used a worktree of this head with src/ checked out from main 1933577, on PG17, and then this head itself.
| am_contracts.sh | test_am_contracts.py | |
|---|---|---|
| main's code | 12 failed of 27 | 6 failed, 1 passed |
| this head | 27/27 | 7 passed |
On main's code each of the four bugs shows exactly as described:
- ON CONFLICT DO NOTHING: "got [0A000|10|v5]".
- The aborted inner block: "got [1|5000] want [5000|12502500]", and the outer delete gave no answer.
- The unique race: "got [ok|2|a] want [23505|1|a]", and the same for the key-changing UPDATE.
- The index build: "got [0] want [1]".
- The DO NOTHING race: 12000 duplicate keys in the shell's run and 87742 in pytest's, against 0 here.
Regression on this head, PG17:
- unique_conc 33, update_conc 25, concurrency 7, phase3 32 (all PASSED);
- native_dml 13/13, index_delete_liveness 9/9, truncate_cleanup 25/25.
CI is 15/15.
What I checked in the code:
- No stale snapshot fields. The new dirty-snapshot branch reads
snapshot->xmin/xmaxas "a catalog read saw an in-progress change". The fetch resets both at entry (columnar_tableam.c:1971on this head), so an earlier candidate can't leave a value behind. - Subtransaction commit is safe. ExecutorEnd now flushes the current subtransaction's buffers only.
SUBXACT_EVENT_COMMIT_SUBalready hands a released child's write states and delete buffers to the parent (write_state.c:3924,delete_vector.c:496), so nothing is stranded under a dead subid. - The build matches heap. It passes
alive=falsefor a deleted row, as heap does for a recently-dead tuple, so a unique build doesn't count it.
Two notes, neither blocking:
pgcolumnar_tuple_complete_speculative's withdrawal flushes withPgColumnarFlushWriteStateForRelation, which isn't subtransaction-aware. A withdrawn speculative insert inside an EXCEPTION block, into a table the enclosing statement has buffered rows for, would flush those rows under the block. That's #1360's class, reached by a narrower path. Worth naming there so its design covers this call too.- The build now indexes every deleted row, however old, where heap skips a tuple dead to all snapshots. That's correct, and you pinned the unique-index cost. The other cost is index size on a table with many old deletes, until a REINDEX. A CHANGELOG line would set the expectation.
071ddec to
192d51b
Compare
|
@OffgridwithJD, rebased onto f601281. The new head is 192d51b, replacing 071ddec, which you approved. I'm getting this one ready while #1365 and #1369 wait for their rebases. It doesn't need to jump them. Why it's one commit now: all nine commits touched TESTS.md and What to check:
Please re-approve 192d51b once CI is green. I'll check that the approval is on that SHA before merging (see #1368). |
|
@jdatcmd I've verified 192d51b:
Its only CI red is 🤖 Generated with Claude Code |
…sh, not to start; main was red on it (#1368) The poll waited for the holder's RowExclusiveLock on rl, which the UPDATE takes at executor start, before it reaches row 7 and takes the class-104 lock. A poll in between read "none|1": main f601281 failed on PG 17, and #1364 on PG 18. The holder now names itself in application_name after its UPDATE returns, and the poll waits for that. A CTE sleep ahead of the row keeps the window open on every run, so a return to the early event fails every time rather than now and then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
|
@OffgridwithJD, I reran the failed 🤖 Generated with Claude Code |
OffgridwithJD
left a comment
There was a problem hiding this comment.
Re-approved at 192d51b. CI is now green (15/15) after main's advisory_lock_class race was fixed by #1390. Earlier I verified this head against the 071ddec I approved: identical outside the count files. am_contracts was 27/27 and test_am_contracts.py 7 passed on PG15 and PG18, and the gate was clean (2487 / 1902 / 585). Note it will need restating against today's main, which has moved since.
🤖 Generated with Claude Code
192d51b to
db2fd08
Compare
|
@OffgridwithJD, thanks for approving 192d51b. It no longer merged (based on f601281), so I rebased onto 6f2031c. The new head is db2fd08.
Please re-approve db2fd08. #1360's fix builds on this, so I'll open it once this lands. 🤖 Generated with Claude Code |
…red first (#1341) Rebased onto f601281 as one commit; the reviewed head was 071ddec, whose nine commits were: 3c23b27 fix: INSERT ... ON CONFLICT DO NOTHING works on a columnar table with a unique index 891032b test: a TRUNCATE that rolls back restores every row, at transaction level and to a savepoint 37881d7 test: rows the parent buffered before a savepoint survive a TRUNCATE rolled back to it 2dad066 fix: an aborted EXCEPTION block no longer loses the enclosing statement's buffered rows 0ccaee8 fix: a unique insert waits for an uncommitted delete of its key instead of duplicating it 0608c09 test: document am_contracts' later tests and count them a155650 fix: an index built after a delete serves an older snapshot that still sees the row 9352ea6 test: count and document the index-snapshot test 071ddec test: ledger rows for am_contracts, seeded from five-major runs and seven mutations The patch is identical to 071ddec outside the count files. Restated on main: TESTS.md section 105 -> 106 (main's 105 is import_export_privilege), cluster_tests 539 -> 546 by collection, ledger 2460 + 27 = 2487 rows, 1893 -> 1902 never. am_contracts 27/27 and test_am_contracts.py 7 passed on each of PG 15-19 on this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
db2fd08 to
859d0f0
Compare
|
@OffgridwithJD, db2fd08 went stale after #1387, #1389 and #1397, so I rebased onto dc11072. The new head is 859d0f0.
Please re-approve 859d0f0. I'll hold every other ledger merge until it's in, then rebase #1403 (#1360) onto it. 🤖 Generated with Claude Code |
…f sleeping The arm from #1364 held a REPEATABLE READ snapshot, slept four seconds, and assumed the DELETE and CREATE INDEX finished inside them. On a loaded box they did not: the plan was taken before the index existed, and 'premise: the older snapshot's lookup is an index scan' read 0 on PG16. A 5-second delay before CREATE INDEX reproduces it on PG17 every time. Now a gate session holds an advisory lock. Session 1 takes its snapshot, names itself, and queues for the lock. The script waits for the name, deletes, builds the index, then ends the gate. With the same delay the suite passes 28/28. A new premise in both twins says the older snapshot counted all 10000 rows, so it predates the delete. The arm still goes red when the index build skips deleted rows again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
Part of #1341, my core-am items, as planned on #1283. A new suite,
am_contracts, in both twins, pins contracts the access method owes core DML and transactions. Writing it found four real bugs, two of them silent data loss or a broken unique constraint. Each is fixed here, and each arm was shown failing against the code before its fix.The bugs
INSERT ... ON CONFLICT DO NOTHINGraised0A000on any table with a unique index;docs/limitations.mdsaid it worksINSERT ... SELECT f(g)of 5000 rows kept 1 ("got [1|5000]"); the matching DELETE gave no answer_bt_check_uniquewaitsPlus one item that was a missing test, not a bug: a TRUNCATE that rolls back, at transaction level and to a savepoint, restores every row. With TRUNCATE emptying the old storage in place, all five of its arms fail.
Proofs beyond the arms
Not fixed here
#1360, the same data loss as (2) but through a read of the enclosing statement's table inside the block. Skipping that flush would give wrong answers instead, so it needs the design proposed there.
Evidence
cluster_tests533 → 540, and the database-free set gives 429 passed.Merge order
Behind #1357, #1359, #1361, #1358 and #1356. I'll rebase and restate when it's its turn.
🤖 Generated with Claude Code