From 665c289a566d27bd7c5804d36f160774f1c78549 Mon Sep 17 00:00:00 2001
From: Salma <salmaabdelmotagaly390@gmail.com>
Date: Wed, 29 Jul 2026 00:09:59 +0300
Subject: [PATCH v1 3/3] amcheck: Add verification for B-tree page merges

Extend verify_nbtree to validate structural invariants for merged
and tombstone leaf pages.
---
 contrib/amcheck/verify_nbtree.c | 162 ++++++++++++++++++++++++++++++--
 1 file changed, 155 insertions(+), 7 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3ef2d66f826..2553ee2b327 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -238,6 +238,8 @@ static ItemId PageGetItemIdCareful(BtreeCheckState *state, BlockNumber block,
 static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 													  IndexTuple itup, bool nonpivot);
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
+static void bt_check_ma_page(BtreeCheckState *state);
+static void bt_check_m_page(BtreeCheckState *state);
 
 /*
  * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
@@ -661,7 +663,7 @@ bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level)
 
 		opaque = BTPageGetOpaque(state->target);
 
-		if (P_IGNORE(opaque))
+		if (P_IGNORE(opaque) || P_ISMERGEDAWAY(opaque))
 		{
 			/*
 			 * Since there cannot be a concurrent VACUUM operation in readonly
@@ -682,6 +684,9 @@ bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level)
 						 errdetail_internal("Block=%u left block=%u left link from block=%u.",
 											current, leftcurrent, opaque->btpo_prev)));
 
+			if (P_ISMERGEDAWAY(opaque))
+				bt_check_ma_page(state);
+
 			if (P_RIGHTMOST(opaque))
 				ereport(ERROR,
 						(errcode(ERRCODE_INDEX_CORRUPTED),
@@ -818,7 +823,7 @@ nextpage:
 		 * splits wasn't investigated yet.  Thankfully we only need low key
 		 * for readonly verification and concurrent splits won't happen.
 		 */
-		if (state->readonly && !P_RIGHTMOST(opaque))
+		if (state->readonly && !P_RIGHTMOST(opaque) && !P_ISMERGEDAWAY(opaque))
 		{
 			IndexTuple	itup;
 			ItemId		itemid;
@@ -1281,6 +1286,9 @@ bt_target_page_check(BtreeCheckState *state)
 		}
 	}
 
+	if (P_ISMERGED(topaque))
+		bt_check_m_page(state);
+
 	/*
 	 * Loop over page items, starting from first non-highkey item, not high
 	 * key (if any).  Most tests are not performed for the "negative infinity"
@@ -1740,7 +1748,7 @@ bt_target_page_check(BtreeCheckState *state)
 					/*
 					 * All !readonly checks now performed; just return
 					 */
-					if (P_IGNORE(topaque))
+					if (P_IGNORE(topaque) || P_ISMERGEDAWAY(topaque))
 						return;
 				}
 
@@ -1790,7 +1798,7 @@ bt_target_page_check(BtreeCheckState *state)
 												  rightblock_number);
 					topaque = BTPageGetOpaque(rightpage);
 
-					if (P_IGNORE(topaque))
+					if (P_IGNORE(topaque) || P_ISMERGEDAWAY(topaque))
 					{
 						pfree(rightpage);
 						break;
@@ -1912,7 +1920,7 @@ bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffs
 		rightpage = palloc_btree_page(state, targetnext);
 		opaque = BTPageGetOpaque(rightpage);
 
-		if (!P_IGNORE(opaque) || P_RIGHTMOST(opaque))
+		if ((!P_IGNORE(opaque) && !P_ISMERGEDAWAY(opaque)) || P_RIGHTMOST(opaque))
 			break;
 
 		/*
@@ -2258,7 +2266,8 @@ bt_child_highkey_check(BtreeCheckState *state,
 		 * If we visit page with high key, check that it is equal to the
 		 * target key next to corresponding downlink.
 		 */
-		if (!rightsplit && !P_RIGHTMOST(opaque) && !P_ISHALFDEAD(opaque))
+		if (!rightsplit && !P_RIGHTMOST(opaque) && !P_ISHALFDEAD(opaque) &&
+			!P_ISMERGEDAWAY(opaque))
 		{
 			BTPageOpaque topaque;
 			IndexTuple	highkey;
@@ -2497,6 +2506,22 @@ bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 									state->targetblock, childblock,
 									LSN_FORMAT_ARGS(state->targetlsn))));
 
+	/*
+	 * A merged-away (MA) page is a tombstone that should have had its parent
+	 * downlink redirected to the merged destination page (R) during the merge
+	 * operation. In readonly mode, no concurrent merge can be in progress, so
+	 * finding a downlink to an MA page indicates corruption.
+	 */
+	if (P_ISMERGEDAWAY(copaque))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("downlink to merged-away page found in index \"%s\"",
+						RelationGetRelationName(state->rel)),
+				 errdetail_internal("Parent block=%u child block=%u parent page lsn=%X/%08X.",
+									state->targetblock, childblock,
+									LSN_FORMAT_ARGS(state->targetlsn))));
+
+
 	for (offset = P_FIRSTDATAKEY(copaque);
 		 offset <= maxoffset;
 		 offset = OffsetNumberNext(offset))
@@ -2611,6 +2636,13 @@ bt_downlink_missing_check(BtreeCheckState *state, bool rightsplit,
 		return;
 	}
 
+	/*
+	 * A merged-away leaf page intentionally lacks a parent downlink, as it
+	 * was unlinked during the custom bt_merge operation.
+	 */
+	if (P_ISMERGEDAWAY(opaque))
+		return;
+
 	/*
 	 * Page under check is probably the "top parent" of a multi-level page
 	 * deletion.  We'll need to descend the subtree to make sure that
@@ -3435,7 +3467,7 @@ palloc_btree_page(BtreeCheckState *state, BlockNumber blocknum)
 				 errmsg_internal("internal page block %u in index \"%s\" has garbage items",
 								 blocknum, RelationGetRelationName(state->rel))));
 
-	if (P_HAS_FULLXID(opaque) && !P_ISDELETED(opaque))
+	if (P_HAS_FULLXID(opaque) && (!P_ISDELETED(opaque) && !P_ISMERGEDAWAY(opaque)))
 		ereport(ERROR,
 				(errcode(ERRCODE_INDEX_CORRUPTED),
 				 errmsg_internal("full transaction id page flag appears in non-deleted block %u in index \"%s\"",
@@ -3591,3 +3623,119 @@ BTreeTupleGetPointsToTID(IndexTuple itup)
 	/* Pivot tuple returns TID with downlink block (heapkeyspace variant) */
 	return &itup->t_tid;
 }
+
+
+static void
+bt_check_ma_page(BtreeCheckState *state)
+{
+	Page		page = state->target;
+	BlockNumber block = state->targetblock;
+	BTPageOpaque opaque = BTPageGetOpaque(page);
+
+
+	if (!P_ISLEAF(opaque))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged-away block %u is not a leaf page in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+
+	if (P_ISROOT(opaque))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged-away block %u cannot be root page in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+
+
+	if (P_ISMERGED(opaque) || P_ISHALFDEAD(opaque) || P_ISDELETED(opaque))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged-away block %u has conflicting page flags in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+
+	if (opaque->btpo_next == P_NONE)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged-away block %u lacks right sibling in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+
+	if (!P_HAS_FULLXID(opaque) ||
+		!FullTransactionIdIsValid(BTMergedAwayGetSafeXid(page)))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged-away block %u has invalid safemergexid in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+}
+
+static void
+bt_check_m_page(BtreeCheckState *state)
+{
+	Page		page = state->target;
+	BlockNumber block = state->targetblock;
+	BTPageOpaque opaque = BTPageGetOpaque(page);
+
+	BlockNumber ma_blkno;
+	Page		ma_page;
+	BTPageOpaque ma_opaque;
+
+	if (!P_ISLEAF(opaque))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged page %u is not a leaf page in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+
+	if (P_ISMERGEDAWAY(opaque))
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged page %u cannot also be a merged-away tombstone in index \"%s\"",
+						block, RelationGetRelationName(state->rel))));
+
+	ma_blkno = BTMergedPageGetMABlkno(page);
+
+	if (!BlockNumberIsValid(ma_blkno) ||
+		ma_blkno == BTREE_METAPAGE ||
+		ma_blkno == block)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged page %u has invalid MA block number %u in index \"%s\"",
+						block, ma_blkno, RelationGetRelationName(state->rel))));
+
+	ma_page = palloc_btree_page(state, ma_blkno);
+	ma_opaque = BTPageGetOpaque(ma_page);
+
+	if (state->readonly && !P_ISMERGEDAWAY(ma_opaque))
+	{
+		pfree(ma_page);
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				 errmsg("merged page %u points to MA block %u which is not a merged-away tombstone in index \"%s\"",
+						block, ma_blkno, RelationGetRelationName(state->rel))));
+	}
+
+	pfree(ma_page);
+
+	/*
+	 * Verify adjacent merge group consistency: In readonly mode (where no
+	 * concurrent VACUUM/merge can occur), if the right sibling is also an M
+	 * page, it must belong to the same merge group (i.e. name the exact same
+	 * MA block number).
+	 */
+	if (state->readonly && opaque->btpo_next != P_NONE)
+	{
+		Page		next_page = palloc_btree_page(state, opaque->btpo_next);
+		BTPageOpaque next_opaque = BTPageGetOpaque(next_page);
+
+		if (P_ISMERGED(next_opaque) &&
+			BTMergedPageGetMABlkno(next_page) != ma_blkno)
+		{
+			BlockNumber next_ma = BTMergedPageGetMABlkno(next_page);
+
+			pfree(next_page);
+			ereport(ERROR,
+					(errcode(ERRCODE_INDEX_CORRUPTED),
+					 errmsg("adjacent merged pages %u and %u have inconsistent MA block numbers (%u vs %u) in index \"%s\"",
+							block, opaque->btpo_next, ma_blkno, next_ma,
+							RelationGetRelationName(state->rel))));
+		}
+		pfree(next_page);
+	}
+}
-- 
2.43.0

