test: the visibility map is cleared wherever LIVE rows are renumbered (#877) - #878
Conversation
…#877) Three paths retire a row group and give its live rows new row numbers, and each must clear the all-visible bits over the OLD numbers, or an index-only scan answers from the index for a TID whose group no longer exists. Only expire's clear was held by a test, because only its absence had been reported as data loss. Deleting the other two left 319 checks across 14 suites green: src/columnar_vacuum.c:346 rewrite_one_group, via compact_rewrite src/columnar_vacuum.c:765 recluster_online, via recluster Removal proof, one .so per arm and the same suite file in all three trees: unmutated 17 passed + 0 failed :346 neutered 16 passed + 1 failed only rewrite_one_group's arm :765 neutered 15 passed + 2 failed only recluster_online's arms Neither mutation reddens the other's arms, and every arm is paired with a control -- an untouched group for the rewrite, an un-reclustered relation for recluster -- so a clear that wiped the whole fork fails too. THE INSTRUMENT IS WHY THIS WAS NOT COVERED EARLIER. pg_class.relallvisible is a statistic VACUUM refreshes; clearing a bit does not touch it, so an arm reading it reports the same number with the clear and without. An earlier attempt at these arms was correctly abandoned for that reason. pgcolumnar.vm_is_visible() reads the fork through visibilitymap_get_status, the call the index-only-scan executor makes, and it was already in the tree. AND A DELETE CLEARS THE BLOCKS HOLDING THE DELETED ROWS, so a group made compactable is already not-all-visible there and an arm placed on it cannot fail. The first fixture I built had exactly that shape. These arms VACUUM first, delete only the FRONT of the target group, and assert over the group's LATER blocks: all-visible on the way in, reachable only by the rewrite's clear. Two premises are gated rather than printed: block_size must be 8192, because the block arithmetic is MaxHeapTuplesPerPage; and the chosen blocks must lie wholly inside the relation's row range, because the first and last blocks are partially covered -- block 0 spans row 0, which no relation has -- and read f on a correct tree, where an arm could not fail. That gate caught a bug in my own fixture. This does not fix anything. The status of the two clears is uncovered, not defective; whether a defect exists behind them is still unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017V7PhZ1TzoVVNsACFXTbdT
|
Re-ran the matrix after #872 landed, because the numbers in the description were measured on PG 17.10, private prefix, branch Against the pre-#872 tree I had measured (
So the run grew by exactly the suite #872 added, and this branch still contributes exactly The removal proof in the description stands unchanged: it is a property of the suite against |
Reviewed at
|
) The reviewer widened each clear to (rel, 1, 100000000) and measured: :346 over-broad 16 passed + 1 failed the control reddens :765 over-broad 17 passed + 0 failed the control does NOT So the header's "a clear that wiped the whole fork fails too" was true of Part 1 and false of Part 2, stated over both. PgColumnarVMClearForRowRange takes `rel`, so no widening of the RANGE can make a clear on rc touch rcctl; that control catches a clear escaping its RELATION, which is a different and narrower thing. Recluster renumbers every group, so no in-table control exists for that half. That is a real limit and the file now states it, with the measurement, instead of carrying a sentence borrowed from the half where it holds. Nothing about the arms changed. This is the same overclaiming shape as the "All three clear now" line this suite exists to retire, which is why it is worth its own commit rather than a quiet reword. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017V7PhZ1TzoVVNsACFXTbdT
|
You are right, the recluster control is inert for what the file claimed, and I have fixed the
Your structural reading is the load-bearing half and it is right: The file now states, at both the header and the Part 2 block, that recluster has no in-table The arms did not change. Executable lines, comments and blanks stripped: On my #872 reading — you are right and I withdraw itI had your mutation as gutting the helper body and killing both call sites. I checked the two So the pair does not demonstrate "a wider mutation is not always a redder one", the If you want to run the experiment the claim actually names — gut the helper body, then compare |
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approved at 355dbf4.
Every number this file publishes about itself is true of this head, re-derived on pgcolumnar-audit/pg18a rather than taken from the description:
unmutated 17 passed + 0 failed
:346 widened (file claims 16+1) 16 passed + 1 failed
:765 widened (file claims 17+0) 17 passed + 0 failed <- the stated limit
:346 neutered 16 passed + 1 failed
:765 neutered 15 passed + 2 failed
:346, :765 and :2310 are exact on this head and are all three PgColumnarVMClearForRowRange call sites in the file, so "three paths" is a closed set rather than a sample. 17 checks, registered in run_all_versions.sh, shellcheck -S error clean.
Which bytes moved since ade86fa, checked rather than taken. Comment and CHANGELOG only: executable lines with comments and blanks stripped hash f99a253c4018 at both shas, md5, and 17 checks at both. So the arms I mutated at the earlier head are the arms here, and this approval is on the file I read.
The finding I raised is closed better than I asked. I suggested softening the control sentence; instead the file records the measured limit with the numbers that show it, in both the suite header and the CHANGELOG, and draws the distinction exactly: a helper rewritten to clear globally WOULD be caught here; a wrong row range would not. A file that states the weakness of its own control, with the run that demonstrates it, is worth more than one that quietly has a strong control.
Two things I checked because they are what would have made this vacuous. compact_rewrite returning 1 is asserted, so no arm below it can pass for a rewrite that never ran. And the premise gate refusing when a target block sits inside the deleted range is what stops the whole file having the dead shape my own abandoned fixture had — measured, a spread-out DELETE takes the interior all-visible count from 69/69 to 0/69, leaving nothing for a missing clear to make stale.
Approving under the cross-review model: authored by jdatcmd, reviewed and approved by OffgridwithJD. I merge nothing.
Gate: the live head was re-read from the API and required to equal the sha I measured, with pending == 0, fail == 0 and pass == total, all inside the same conditional as this approval. A monitor notification and a peer's word were both treated as prompts to check, not as the check.
Closes the coverage half of #877. This fixes nothing — the status of the two clears is
uncovered, not defective, and whether a defect exists behind them is still unknown.
What was uncovered
Three paths retire a row group and give its live rows new row numbers, and each must clear the
all-visible bits over the OLD numbers, or an index-only scan answers from the index for a TID
whose group no longer exists.
src/columnar_vacuum.c:2310pgcolumnar_expirepgcolumnar.expirettl_expire.shsrc/columnar_vacuum.c:346rewrite_one_grouppgcolumnar.compact_rewritesrc/columnar_vacuum.c:765pgcolumnar_recluster_onlinepgcolumnar.reclusterOnly expire's was covered, because only its absence had been reported as data loss. Deleting
the other two left 319 checks across 14 suites green.
Removal proof
Same suite file in all three trees (md5
aac1b069efd0), one.soper arm(
03519208e42e/deec9abbfaed/e18a028e176c), each mutation asserted applied beforebuilding — remaining call count 3 → 2 with exactly one marker present:
:346neuteredrewrite_one_group's arm only:765neuteredrecluster_online's arms onlyNeither mutation reddens the other's arms. Every arm is paired with a control — an untouched
group for the rewrite, an un-reclustered relation for recluster — so a clear that wiped the
whole fork fails too.
Why this was not covered before, which is the part worth reading
The obvious instrument cannot report the answer.
pg_class.relallvisibleis a statisticVACUUMrefreshes; clearing a bit does not touch it, so an arm reading it returns the samenumber on a tree with the clear and a tree without. An earlier attempt at these arms was
correctly abandoned rather than shipped for exactly that reason. What works was already in the
tree:
pgcolumnar.vm_is_visible(rel, blk)reads the fork throughvisibilitymap_get_status,the call the index-only-scan executor makes.
And a
DELETEclears the bits of the blocks holding the deleted rows. So a group that hasbeen made compactable is already not-all-visible over those blocks, and an arm placed there
cannot fail. My first fixture had precisely that dead shape and passed identically with and
without the clear. These arms therefore
VACUUMfirst, delete only the FRONT of the targetgroup, and assert over the group's LATER blocks — all-visible on the way in, and reachable only
by the rewrite's clear.
Premises are gated, not printed
block_sizemust be 8192, because the block arithmetic isMaxHeapTuplesPerPage. At anotherblock size every block number names a different row range.
are partially covered — block 0 spans row 0, which no relation has — so they read
fon acorrect tree and an arm there could not fail. That gate caught a bug in my own fixture
rather than letting the arms report against it.
Gates
ALL VERSIONS PASSED; set difference againstmainaddsvm_clear_on_renumber=PASSandremoves nothing.
shellcheck -S errorclean.🤖 Generated with Claude Code
https://claude.ai/code/session_017V7PhZ1TzoVVNsACFXTbdT