Repository navigation
fix: REINDEX reclaims the entries of deleted rows no snapshot can see (#1407) - #1412
Merged
Merged
Conversation
This was referenced Oct 15, 2026
jdatcmd
force-pushed
the
fix/1407-reindex-reclaims
branch
from
October 15, 2026 02:44
2b50ffd to
b1a8b64
Compare
Collaborator
Author
|
Rebased onto 217f23b (#1408), with the ledger paragraph restated: 2774 + 1 = 2775 rows, 2095 never, 679 + 1 = 680 dated. |
OffgridwithJD
approved these changes
Oct 15, 2026
OffgridwithJD
left a comment
Collaborator
There was a problem hiding this comment.
Approved at b1a8b64, with full CI green (15/15).
- The fix: the index build reads each group's delete marks a second time, under a snapshot with xmin = xmax =
GetOldestNonRemovableTransactionId(rel), the horizonheapam_index_build_range_scanuses. Rows deleted before that horizon are left out, and rows deleted after it are still emitted as not alive. The snapshot is registered around the build. The read state is zeroed, so no other reader carries a horizon, and the dead mask lives in the group context like the delete mask. - Reproduced red first (PG17): on main's src,
am_contracts.shgives 27 + 1 failed:REINDEX after a committed delete of most rows reclaims their entries, as heap's does: got [kept:2260992->2260992|reclaimed|10000] want [reclaimed|reclaimed|10000].test_am_contracts.pygives 1 failed, 7 passed, by the same name. - Green: 28/28 and 8 passed on PG17 and PG18. The census, 2775 / 2095 / 680, matches the tsv, and
cluster_tests566 matches collection. - The other direction reproduced (PG17): with the horizon at
ReadNextTransactionId(), 27 + 1 failed:an index built after a delete finds the row for an older snapshot that sees it: got [0] want [1], and the twin is red by name. So the two arms bracket the horizon. - Not blocking: #1422 also moves
cluster_tests565 -> 566 and the ledger from 2774, so whichever lands second restates both. The Unreleased #1341 entry still says "Deleted rows are now indexed as not alive"; "deleted rows some snapshot can still see" would keep it true.
🤖 Generated with Claude Code
…1341 item 50) Two finite float8s, or two finite reals, whose sum is infinite raise "value out of range: overflow" (22003) in core's float8pl and float4pl. The grouped node's accumulators carry the same check, and nothing tested it: only avg's overflow had an arm, in the shell alone. native_groupagg gains the sum arm in both twins, judged by SQLSTATE against core with the node off, plus a check that the node fires. Removing either accumulator's check reddens its own position. native_groupagg enters the ledger seeded; suites_not_covered 234 -> 233. cluster_tests +1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
…#1407) older snapshot still finds a recently deleted row by a new index. But it emitted every deleted row, where heap's build skips a tuple dead to all. So REINDEX, documented as what reclaims stale entries, reclaimed none: 9 rows in 10 deleted, and the rebuilt index kept its size where heap's shrank to a tenth. The build now also reads each row group's delete marks under a horizon snapshot (xmin = xmax = the oldest non-removable xid, curcid 0), which sees exactly the transactions committed before every running snapshot began, and leaves out a row deleted there. The snapshot is registered: PG 18+ asserts it in the visibility check. am_contracts gains the arm in both twins, red on main (kept:2260992-> 2260992); with the horizon moved to the next xid, the older-snapshot arm goes red instead. limitations.md says when REINDEX keeps an entry. cluster_tests +1. Fixes #1407. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
jdatcmd
force-pushed
the
fix/1407-reindex-reclaims
branch
from
October 15, 2026 16:38
b1a8b64 to
e8a1af5
Compare
OffgridwithJD
approved these changes
Oct 15, 2026
OffgridwithJD
left a comment
Collaborator
There was a problem hiding this comment.
Re-approved at e8a1af5, stacked on #1411.
- Same patch: this PR's own commit (3ecde7f..e8a1af5) matches what I approved at b1a8b64, outside the count files and ledger rows included.
- Census: 2968 / 2272 / 696 from the tsv, which matches the paragraph's 2967 + 1, 2272 and 695 + 1.
- CI: 12 checks green and 3 still running at the time of review.
🤖 Generated with Claude Code
This was referenced Oct 15, 2026
This was referenced Oct 15, 2026
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.
Fixes #1407. This is a regression from my #1364 (bug 4). It also covers item 37 on #1341: REINDEX is documented and now runs in a test.
The bug
#1364 made the columnar index build emit deleted rows as "not alive", so an older snapshot still finds a recently deleted row through a new index. But it emitted every deleted row, where heap's build skips a tuple that is dead to all. So
REINDEX, which limitations.md documents as what reclaims stale entries, reclaimed none:REINDEXThe fix
The build also reads each row group's delete marks under a horizon snapshot:
xmin = xmax =GetOldestNonRemovableTransactionId(rel), withcurcid0. That snapshot sees exactly the transactions committed before every running snapshot began. A row deleted under it is dead to all, so it is left out, as heap leaves a dead tuple out. A row deleted after the horizon is still emitted as not alive.Implementation:
PgColumnarReadSetDeadHorizonand a second per-group mask, built only when the group has deletes;Evidence (both twins, am_contracts)
test_am_contracts.py8 passed on eachan index built after a delete finds the row for an older snapshot that sees it: got [0] want [1], in both twinsThe two directions bracket the horizon. Too early, and REINDEX reclaims nothing. Too late, and an older snapshot loses a row it can see.
Index suites on PG16 and PG18, all passing: index_delete_liveness, native_index, index_am_support, native_ios, catalog_delete_index, catalog_scan_index, unique_conc, native_delete_vector_index, parallel_am_scan, native_recluster, native_rewrite, hilbert_cluster and estimate_deleted. The guard set gives 433 passed.
Bookkeeping
cluster_tests564 → 565. TESTS.md gains the row. limitations.md says REINDEX keeps a deleted row's entry only while a transaction that began before the delete is still running.🤖 Generated with Claude Code
https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N