From 7ed9738b2a8d2471673fbff88418fcfe411842f2 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Mon, 14 Sep 2026 12:58:42 -0400 Subject: [PATCH v5 2/3] Assert the reorder queue keeps a tuple's TID in the slot IndexNextWithReorder() re-stores a queued tuple with ExecForceStoreHeapTuple(), and slot_getsysattr() answers SelfItemPointerAttributeNumber out of tts_tid alone, so a slot that loses the TID silently projects a different ctid than the row it returned. Assert that the slot advertises the TID the tuple was fetched from. The invariant does not hold for slots in general, since HOT can legitimately make tts_tid and the stored tuple's t_self differ, so the check is confined to this path, where a divergence changes query results. The TID is captured before the store, which frees the tuple, and the comparison uses the NoCheck accessors so that the sentinel trips this assertion rather than the validity check inside ItemPointerEquals(). Suggested-by: Andres Freund --- src/backend/executor/nodeIndexscan.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c index 129d005f187..4bd52405d80 100644 --- a/src/backend/executor/nodeIndexscan.c +++ b/src/backend/executor/nodeIndexscan.c @@ -250,11 +250,32 @@ IndexNextWithReorder(IndexScanState *node) node) <= 0) { HeapTuple tuple; + ItemPointerData tid PG_USED_FOR_ASSERTS_ONLY; tuple = reorderqueue_pop(node); + /* Remember the TID; the store below frees the tuple. */ + tid = tuple->t_self; + /* Pass 'true', as the tuple in the queue is a palloc'd copy */ ExecForceStoreHeapTuple(tuple, slot, true); + + /* + * The tuple came from the heap through this scan, so the slot + * must advertise the TID it was fetched from. If the two + * diverge the scan projects a different ctid than the row it + * returned, which changes query results. This does not hold + * for slots in general, since HOT can legitimately make them + * differ, so assert it only here. + * + * Compare with the NoCheck accessors so that a slot left + * holding the invalid-TID sentinel trips this assertion rather + * than the validity one inside ItemPointerEquals(). + */ + Assert(ItemPointerGetBlockNumberNoCheck(&slot->tts_tid) == + ItemPointerGetBlockNumberNoCheck(&tid) && + ItemPointerGetOffsetNumberNoCheck(&slot->tts_tid) == + ItemPointerGetOffsetNumberNoCheck(&tid)); return slot; } } -- 2.50.1