From 4dd227f51d461b6cbbcdc7d5805ec6ef4023a715 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Sat, 22 Aug 2026 14:50:33 -0400 Subject: [PATCH] Fix GIN posting tree page deletion with incomplete splits. GIN posting tree page deletion failed to consider whether the target page's left sibling page, or the deletion target itself, was marked as incompletely split. Page deletion finds the target page's left sibling by walking the parent's downlinks, but an incompletely split page's new right half is part of the sibling chain despite having no downlink. Deletion could therefore overwrite the rightlink of the wrong page, disconnecting the split's still-live right half from the sibling chain. Scans would then silently miss tuples from that page. To fix, teach the relevant page deletion path to avoid deleting a posting tree page whose left sibling is marked incompletely split (and to avoid doing so when the target page itself is so marked). This is essentially the same approach used by nbtree page deletion. Claude Code found this problem. The committed test case is a simplified version of the one that it wrote to demonstrate this bug. Author: Peter Geoghegan Reviewed-by: Andrey Borodin Discussion: https://postgr.es/m/CAH2-Wz=sKJcn+OtfVN9rdg+Ps9e4cuQWNP-9t12UE2d8nEG90Q@mail.gmail.com Backpatch-through: 14 --- src/backend/access/gin/ginvacuum.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c index 9dc8427a8e0..bfc07110067 100644 --- a/src/backend/access/gin/ginvacuum.c +++ b/src/backend/access/gin/ginvacuum.c @@ -153,6 +153,8 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn page = BufferGetPage(dBuffer); rightlink = GinPageGetOpaque(page)->rightlink; + Assert(GinPageGetOpaque(BufferGetPage(lBuffer))->rightlink == deleteBlkno); + /* * Any insert which would have gone on the leaf block will now go to its * right sibling. @@ -307,8 +309,21 @@ ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot, if (isempty) { - /* we never delete the left- or rightmost branch */ - if (BufferIsValid(me->leftBuffer) && !GinPageRightMost(page)) + /* + * Proceed to the ginDeletePage() if target page is not the leftmost + * or the rightmost page. + * + * leftBuffer is the target's left sibling according to the parent + * level, which is not necessarily its left sibling in the sibling + * link chain (the rightlinks stored on pages): the new right half of + * an incompletely split page is in the sibling chain, but has no + * downlink yet. ginDeletePage isn't prepared to deal with that, so + * we must refuse to delete when either the target or its left + * sibling page is marked incompletely split. + */ + if (BufferIsValid(me->leftBuffer) && !GinPageRightMost(page) && + !GinPageIsIncompleteSplit(page) && + !GinPageIsIncompleteSplit(BufferGetPage(me->leftBuffer))) { Assert(!isRoot); ginDeletePage(gvs, blkno, BufferGetBlockNumber(me->leftBuffer),