From b856c0d12cd8118ed67d91c5e802e44cbc2b05d7 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Thu, 26 Feb 2026 09:32:06 +0500
Subject: [PATCH v4 2/5] amcheck: report corruption when index points to
 non-existent heap tuple

Use SnapshotAny to distinguish "tuple doesn't exist" (corruption) from
"tuple exists but is dead" (skip).  When table_tuple_fetch_row_version
with SnapshotAny returns false, the heap slot was reclaimed or the page
was reorganized (e.g. by VACUUM), so the index has an orphaned entry.
Report that as corruption instead of silently skipping.
---
 contrib/amcheck/verify_nbtree.c | 26 ++++++++++++++++++++++----
 1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index ec6297d21b3..29ce35dcd6c 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -2982,8 +2982,12 @@ bt_verify_index_tuple_points_to_heap(BtreeCheckState *state, IndexTuple itup,
 	/*
 	 * Bloom filter says (key, tid) not in heap.  Follow TID to verify; this
 	 * amortizes random heap lookups when the filter has false negatives, or
-	 * reports corruption when the index points to wrong heap tuple.  Skip
-	 * dead tuples (table_tuple_fetch_row_version returns false for them).
+	 * reports corruption when the index points to wrong heap tuple.
+	 *
+	 * Use SnapshotAny first to distinguish "tuple doesn't exist" (corruption)
+	 * from "tuple exists but is dead" (skip).  SnapshotAny returns any tuple
+	 * at the TID; if that fails, the slot was reclaimed or the page was
+	 * reorganized (e.g. by VACUUM), so the index has an orphaned entry.
 	 */
 	{
 		TupleTableSlot *slot;
@@ -2997,11 +3001,25 @@ bt_verify_index_tuple_points_to_heap(BtreeCheckState *state, IndexTuple itup,
 
 		slot = table_slot_create(state->heaprel, NULL);
 		found = table_tuple_fetch_row_version(state->heaprel, tid,
-											  state->snapshot, slot);
+											  SnapshotAny, slot);
 		if (!found)
 		{
 			ExecDropSingleTupleTableSlot(slot);
-			return;			/* dead or non-existent heap tuple, skip */
+			ereport(ERROR,
+					(errcode(ERRCODE_INDEX_CORRUPTED),
+					 errmsg("index tuple in index \"%s\" points to non-existent heap tuple",
+							RelationGetRelationName(state->rel)),
+					 errdetail_internal("Index tid=(%u,%u) points to heap tid=(%u,%u) that no longer exists.",
+									   targetblock, offset,
+									   ItemPointerGetBlockNumber(tid),
+									   ItemPointerGetOffsetNumber(tid))));
+		}
+
+		/* Skip dead tuples (not visible to our snapshot) */
+		if (!table_tuple_satisfies_snapshot(state->heaprel, slot, state->snapshot))
+		{
+			ExecDropSingleTupleTableSlot(slot);
+			return;
 		}
 
 		indexinfo = state->indexinfo;
-- 
2.51.2

