From ec3559eccf6d7ee5243f7545e8c60e7ab8b0a246 Mon Sep 17 00:00:00 2001
From: Salma <salmaabdelmotagaly390@gmail.com>
Date: Tue, 15 Sep 2026 12:54:57 +0300
Subject: [PATCH v3 1/3] nbtree: Add leaf page merge support

Add support for merging 2 adjacent leaf pages in B-tree indexes to reduce index bloat.  Tuples from the left leaf page (L) are
copied to the right leaf page (R), and downlinks in the parent are updated
accordingly.

Index scan logic is updated to handle concurrent merges by detecting
merged-away tombstone pages and recovering scan positions forward and
backward across merged page groups.

L is marked as BTP_MERGED_AWAY (tombstone) and R as BTP_MERGED.  VACUUM
clears BTP_MERGED flags from merged pages once safe, and converts tombstone
pages to half-dead (BTP_HALF_DEAD) for final page deletion.

WAL logging is fully implemented to ensure crash safety and replication.
Due to opcode exhaustion in the primary B-tree resource manager, a new
resource manager (RM_BTREE2_ID) is introduced specifically for merge
operations, encompassing records for page merges, clearing merge flags,
and marking tombstones as half-dead.
---
 src/backend/access/nbtree/Makefile    |   1 +
 src/backend/access/nbtree/meson.build |   1 +
 src/backend/access/nbtree/nbtdedup.c  |   6 +-
 src/backend/access/nbtree/nbtinsert.c |  40 +-
 src/backend/access/nbtree/nbtmerge.c  | 521 ++++++++++++++++++++++++++
 src/backend/access/nbtree/nbtpage.c   |  84 ++++-
 src/backend/access/nbtree/nbtree.c    | 263 +++++++++++++
 src/backend/access/nbtree/nbtsearch.c | 449 +++++++++++++++++++++-
 src/backend/access/nbtree/nbtutils.c  |   2 +-
 src/backend/access/nbtree/nbtxlog.c   | 203 +++++++++-
 src/backend/access/rmgrdesc/nbtdesc.c |  55 +++
 src/bin/pg_waldump/t/001_basic.pl     |   3 +-
 src/include/access/nbtree.h           | 119 ++++++
 src/include/access/nbtxlog.h          |  47 ++-
 src/include/access/rmgrlist.h         |   1 +
 15 files changed, 1776 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/access/nbtree/nbtmerge.c

diff --git a/src/backend/access/nbtree/Makefile b/src/backend/access/nbtree/Makefile
index 0daf640af96..5593bb9c05f 100644
--- a/src/backend/access/nbtree/Makefile
+++ b/src/backend/access/nbtree/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	nbtcompare.o \
 	nbtdedup.o \
 	nbtinsert.o \
+	nbtmerge.o \
 	nbtpage.o \
 	nbtpreprocesskeys.o \
 	nbtreadpage.o \
diff --git a/src/backend/access/nbtree/meson.build b/src/backend/access/nbtree/meson.build
index 812f067e710..79d225814d0 100644
--- a/src/backend/access/nbtree/meson.build
+++ b/src/backend/access/nbtree/meson.build
@@ -4,6 +4,7 @@ backend_sources += files(
   'nbtcompare.c',
   'nbtdedup.c',
   'nbtinsert.c',
+  'nbtmerge.c',
   'nbtpage.c',
   'nbtpreprocesskeys.c',
   'nbtreadpage.c',
diff --git a/src/backend/access/nbtree/nbtdedup.c b/src/backend/access/nbtree/nbtdedup.c
index af7affdf409..a33e30f743e 100644
--- a/src/backend/access/nbtree/nbtdedup.c
+++ b/src/backend/access/nbtree/nbtdedup.c
@@ -119,6 +119,8 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz,
 	 */
 	newpage = PageGetTempPageCopySpecial(page);
 	PageSetLSN(newpage, PageGetLSN(page));
+	if (P_ISMERGED(opaque))
+		BTMergedPageSetMABlkno(newpage, BTMergedPageGetMABlkno(page));
 
 	/* Copy high key, if any */
 	if (!P_RIGHTMOST(opaque))
@@ -246,10 +248,12 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz,
 	/* XLOG stuff */
 	if (RelationNeedsWAL(rel))
 	{
-		xl_btree_dedup xlrec_dedup;
+		xl_btree_dedup xlrec_dedup = {0};
 
 		xlrec_dedup.nintervals = state->nintervals;
 
+		xlrec_dedup.merged_ma_blkno = P_ISMERGED(opaque)? BTMergedPageGetMABlkno(page): InvalidBlockNumber;
+
 		XLogBeginInsert();
 		XLogRegisterBuffer(0, buf, REGBUF_STANDARD);
 		XLogRegisterData(&xlrec_dedup, SizeOfBtreeDedup);
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 3b945342d83..fa3164aa7f6 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -61,7 +61,6 @@ static Buffer _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key,
 						IndexTuple nposting, uint16 postingoff);
 static void _bt_insert_parent(Relation rel, Relation heaprel, Buffer buf,
 							  Buffer rbuf, BTStack stack, bool isroot, bool isonly);
-static void _bt_freestack(BTStack stack);
 static Buffer _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf);
 static inline bool _bt_pgaddtup(Page page, Size itemsize, const IndexTupleData *itup,
 								OffsetNumber itup_off, bool newfirstdataitem);
@@ -750,7 +749,7 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 				nbuf = _bt_relandgetbuf(rel, nbuf, nblkno, BT_READ);
 				page = BufferGetPage(nbuf);
 				opaque = BTPageGetOpaque(page);
-				if (!P_IGNORE(opaque))
+				if (!P_IGNORE(opaque) && !P_ISMERGEDAWAY(opaque))
 					break;
 				if (P_RIGHTMOST(opaque))
 					elog(ERROR, "fell off the end of index \"%s\"",
@@ -1071,7 +1070,7 @@ _bt_stepright(Relation rel, Relation heaprel, BTInsertState insertstate,
 			continue;
 		}
 
-		if (!P_IGNORE(opaque))
+		if (!P_IGNORE(opaque) && !P_ISMERGEDAWAY(opaque))
 			break;
 		if (P_RIGHTMOST(opaque))
 			elog(ERROR, "fell off the end of index \"%s\"",
@@ -1585,6 +1584,21 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf,
 	lopaque->btpo_level = oopaque->btpo_level;
 	/* handle btpo_cycleid after rightpage buffer acquired */
 
+	/*
+	 * If the original page was BTP_MERGED, the left-half temp page inherits
+	 * BTP_MERGED via the btpo_flags copy above.  However, the MA block number
+	 * stored in pd_prune_xid (see BTMergedPageSetMABlkno) lives in
+	 * PageHeaderData, not BTPageOpaqueData, so it is NOT carried over by the
+	 * btpo_flags assignment.  leftpage was freshly initialized by
+	 * _bt_pageinit above, leaving pd_prune_xid as InvalidTransactionId (0).
+	 * When leftpage is later copied back into origpage (memcpy at
+	 * START_CRIT_SECTION time), the original MA block number would be
+	 * silently lost, causing amcheck to report "merged page N has invalid MA
+	 * block number 0".  Propagate it now.
+	 */
+	if (P_ISMERGED(oopaque))
+		BTMergedPageSetMABlkno(leftpage, BTMergedPageGetMABlkno(origpage));
+
 	/*
 	 * Copy the original page's LSN into leftpage, which will become the
 	 * updated version of the page.  We need this because XLogInsert will
@@ -1773,6 +1787,20 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf,
 	ropaque->btpo_level = oopaque->btpo_level;
 	ropaque->btpo_cycleid = lopaque->btpo_cycleid;
 
+	/*
+	 * If the original page was BTP_MERGED, the new right-half page inherits
+	 * BTP_MERGED via the btpo_flags copy above.  However, the MA_blkno we
+	 * stored in pd_prune_xid (see BTMergedPageSetMABlkno) lives in
+	 * PageHeaderData, not BTPageOpaqueData, so it is NOT copied by the
+	 * btpo_flags assignment.  The right page was freshly allocated by
+	 * _bt_allocbuf, which calls _bt_pageinit -> PageInit, leaving
+	 * pd_prune_xid as InvalidTransactionId (0).  Propagate the MA_blkno now
+	 * so that backward scans can verify the correct tombstone for both halves
+	 * of the split merge group.
+	 */
+	if (P_ISMERGED(oopaque))
+		BTMergedPageSetMABlkno(rightpage, BTMergedPageGetMABlkno(origpage));
+
 	/*
 	 * Add new high key to rightpage where necessary.
 	 *
@@ -1996,9 +2024,11 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf,
 	/* XLOG stuff */
 	if (RelationNeedsWAL(rel))
 	{
-		xl_btree_split xlrec;
+		xl_btree_split xlrec = {0};
 		uint8		xlinfo;
 
+		xlrec.merged_ma_blkno = P_ISMERGED(oopaque)? BTMergedPageGetMABlkno(origpage): InvalidBlockNumber;
+
 		xlrec.level = ropaque->btpo_level;
 		/* See comments below on newitem, orignewitem, and posting lists */
 		xlrec.firstrightoff = firstrightoff;
@@ -2457,7 +2487,7 @@ _bt_getstackbuf(Relation rel, Relation heaprel, BTStack stack, BlockNumber child
 /*
  * _bt_freestack() -- free a retracement stack made by _bt_search_insert.
  */
-static void
+void
 _bt_freestack(BTStack stack)
 {
 	BTStack		ostack;
diff --git a/src/backend/access/nbtree/nbtmerge.c b/src/backend/access/nbtree/nbtmerge.c
new file mode 100644
index 00000000000..509bfb51439
--- /dev/null
+++ b/src/backend/access/nbtree/nbtmerge.c
@@ -0,0 +1,521 @@
+/*-------------------------------------------------------------------------
+ *
+ * nbtmerge.c
+ *	  Merge two adjacent underutilized leaf pages into one to reduce bloat.
+ *
+ * The merge scans leaf pages left-to-right and, when a consecutive pair both
+ * fall below a minimum fill threshold and their combined data fits within the
+ * target fill factor, copies all tuples from the left page (L) into the right
+ * page (R), redirects L's parent downlink to R, removes R's now-redundant
+ * downlink from the parent, marks L as BTP_MERGED_AWAY (tombstone), and marks
+ * R as BTP_MERGED.  VACUUM is responsible for later reclaiming tombstone pages.
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/nbtree/nbtmerge.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "access/tableam.h"
+#include "access/nbtxlog.h"
+#include "access/xloginsert.h"
+#include "common/int.h"
+#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
+#include "utils/injection_point.h"
+#include "storage/predicate.h"
+#include "miscadmin.h"
+
+typedef struct BTMergeState
+{
+
+	Relation	rel;
+	float8		min_threshold;	/* minimum fill fraction to qualify for merge */
+	float8		fillfactor;		/* maximum combined fill fraction after merge */
+	BlockNumber left_blkno;
+	BlockNumber right_blkno;
+	BTStack		stack;
+}			BTMergeState;
+
+
+static int32 _bt_mergescan(Relation rel, float8 min_threshold, float8 fillfactor, int pages_limit);
+static bool _bt_mergepage(BTMergeState mstate);
+static BTScanInsert _bt_merge_mkscankey(Relation rel, Page page, BTPageOpaque opaque);
+static bool _bt_pages_mergeable(Page leftpage, Page rightpage, float8 min_threshold, float8 fillfactor);
+
+
+/*
+ * _bt_merge_index() -- Entry point: merge underutilized leaf pages.
+ *
+ * min_pct is the per-page threshold (0..100); pages below this are candidates.
+ * dest_pct is the target combined fill (0..100); the merged page must fit.
+ * num_pages caps how many leaf pairs are examined in one call.
+ *
+ * Returns the number of merges actually performed.
+ */
+int32
+_bt_merge_index(Relation rel, float8 min_pct, float8 dest_pct, int32 num_pages)
+{
+	return _bt_mergescan(rel, min_pct, dest_pct, num_pages);
+}
+
+
+/*
+ * _bt_merge_mkscankey() -- Build a BTScanInsert key from the first data tuple
+ * on a leaf page.  Returns NULL if the page carries no data tuples.  Caller
+ * must pfree the returned key.
+ */
+static BTScanInsert
+_bt_merge_mkscankey(Relation rel, Page page, BTPageOpaque opaque)
+{
+	OffsetNumber first_off = P_FIRSTDATAKEY(opaque);
+
+	if (first_off > PageGetMaxOffsetNumber(page))
+		return NULL;
+
+	return _bt_mkscankey(rel,
+						 (IndexTuple) PageGetItem(page,
+												  PageGetItemId(page, first_off)));
+}
+
+/*
+ * _bt_pages_mergeable() -- Return true when the pair qualifies for merging.
+ *
+ * Both pages must individually be below min_threshold and their combined used
+ * space must fit within fillfactor.  Thresholds are fractions (0.0 - 1.0).
+ */
+static bool
+_bt_pages_mergeable(Page leftpage, Page rightpage,
+					float8 min_threshold, float8 fillfactor)
+{
+	BTPageOpaque leftopaque = BTPageGetOpaque(leftpage);
+	Size		left_used = BLCKSZ - PageGetFreeSpace(leftpage);
+	Size		right_used = BLCKSZ - PageGetFreeSpace(rightpage);
+	Size		bytes_needed = 0;
+	OffsetNumber maxoff_left = PageGetMaxOffsetNumber(leftpage);
+	OffsetNumber first_left = P_FIRSTDATAKEY(leftopaque);
+
+	/* Check individual threshold qualifications first */
+	if ((float8) left_used / BLCKSZ > min_threshold ||
+		(float8) right_used / BLCKSZ > min_threshold)
+		return false;
+
+	/* Compute exact bytes needed for all data tuples transferred from L */
+	for (OffsetNumber off = first_left; off <= maxoff_left; off++)
+	{
+		ItemId		itemid = PageGetItemId(leftpage, off);
+		IndexTuple	itup = (IndexTuple) PageGetItem(leftpage, itemid);
+
+		bytes_needed += MAXALIGN(IndexTupleSize(itup)) + sizeof(ItemIdData);
+	}
+
+	/* Ensure R has enough physical free space to hold all transferred tuples */
+	if (PageGetFreeSpace(rightpage) < bytes_needed)
+		return false;
+
+	/* Ensure total resulting size fits within the specified fillfactor */
+	if ((float8) (right_used + bytes_needed) / BLCKSZ > fillfactor)
+		return false;
+
+	return true;
+}
+
+
+/*
+ * _bt_mergescan() -- Walk leaf pages left-to-right looking for merge
+ * candidates.
+ *
+ * Scans leaf pages starting from the second leftmost.  For each
+ * candidate pair (L, R), verifies they share a parent, then calls
+ * _bt_mergepage() to perform the actual merge under exclusive locks.
+ *
+ * Stops after pages_limit leaf pages have been examined or the rightmost leaf
+ * is reached.  Returns the number of merges performed.
+ */
+static int32
+_bt_mergescan(Relation rel, float8 min_threshold, float8 fillfactor, int pages_limit)
+{
+	Buffer		leftbuf,
+				rightbuf;
+	Page		leftpage,
+				rightpage,
+				temp_page;
+	BTPageOpaque leftopaque,
+				rightopaque,
+				temp_opaque;
+	BlockNumber left_blkno,
+				right_blkno,
+				current_blkno,
+				r_right_blkno;
+	int32		merges_performed = 0;
+	int			num_pages = 0;
+	BTScanInsert scankey;
+	BTMergeState mstate;
+	BTStack		stack;
+
+	mstate.rel = rel;
+	mstate.min_threshold = min_threshold / 100.0;
+	mstate.fillfactor = fillfactor / 100.0;
+
+	/* Start from the second leftmost leaf page. */
+	{
+		Buffer		endpoint_buf = _bt_get_endpoint(rel, 0, false);
+
+		current_blkno = BufferGetBlockNumber(endpoint_buf);
+		temp_page = BufferGetPage(endpoint_buf);
+		temp_opaque = BTPageGetOpaque(temp_page);
+		current_blkno = temp_opaque->btpo_next;
+		UnlockReleaseBuffer(endpoint_buf);
+	}
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (num_pages >= pages_limit || current_blkno == P_NONE)
+			return merges_performed;
+
+		/* Pin and share-lock the left candidate. */
+		left_blkno = current_blkno;
+		leftbuf = ReadBuffer(rel, left_blkno);
+		LockBuffer(leftbuf, BUFFER_LOCK_SHARE);
+		leftpage = BufferGetPage(leftbuf);
+		leftopaque = BTPageGetOpaque(leftpage);
+
+		if (P_RIGHTMOST(leftopaque))
+		{
+			UnlockReleaseBuffer(leftbuf);
+			return merges_performed;
+		}
+
+		if (P_ISDELETED(leftopaque) || P_ISHALFDEAD(leftopaque)
+			|| P_ISMERGED(leftopaque) || P_ISMERGEDAWAY(leftopaque))
+		{
+			current_blkno = leftopaque->btpo_next;
+			UnlockReleaseBuffer(leftbuf);
+			continue;
+		}
+
+		Assert(P_ISLEAF(leftopaque));
+
+		/* Pin and share-lock the right candidate. */
+		right_blkno = leftopaque->btpo_next;
+		rightbuf = ReadBuffer(rel, right_blkno);
+		LockBuffer(rightbuf, BUFFER_LOCK_SHARE);
+		rightpage = BufferGetPage(rightbuf);
+		rightopaque = BTPageGetOpaque(rightpage);
+
+		if (P_ISDELETED(rightopaque) || P_ISHALFDEAD(rightopaque)
+			|| P_ISMERGED(rightopaque) || P_ISMERGEDAWAY(rightopaque))
+		{
+			current_blkno = rightopaque->btpo_next;
+			UnlockReleaseBuffer(rightbuf);
+			UnlockReleaseBuffer(leftbuf);
+			continue;
+		}
+
+		Assert(P_ISLEAF(rightopaque));
+
+		/* Save R's right sibling while we still hold the share lock on R. */
+		r_right_blkno = rightopaque->btpo_next;
+
+		/* L is examined; slide the window to R as the default next-left. */
+		num_pages++;
+		current_blkno = right_blkno;
+
+		scankey = _bt_merge_mkscankey(rel, leftpage, leftopaque);
+
+		if (_bt_pages_mergeable(leftpage, rightpage,
+								mstate.min_threshold, mstate.fillfactor) &&
+			scankey != NULL)
+		{
+			/*
+			 * R is also consumed; advance past it.
+			 *
+			 * Drop share locks before calling _bt_pages_share_parent.
+			 * _bt_search descends to the left leaf and will try to lock it,
+			 * which would fail an assertion if we already hold a lock on it.
+			 * The scankey is a palloc'd copy so releasing here is safe.
+			 */
+			num_pages++;
+			current_blkno = r_right_blkno;
+			UnlockReleaseBuffer(rightbuf);
+			UnlockReleaseBuffer(leftbuf);
+
+			if (_bt_pages_share_parent(rel, left_blkno, right_blkno,
+									   scankey, &stack))
+			{
+				mstate.left_blkno = left_blkno;
+				mstate.right_blkno = right_blkno;
+				mstate.stack = stack;
+
+				if (_bt_mergepage(mstate))
+					merges_performed++;
+
+				_bt_freestack(stack);
+			}
+
+			pfree(scankey);
+			continue;
+		}
+
+		/* Pages don't qualify; current_blkno already points at R. */
+		UnlockReleaseBuffer(rightbuf);
+		UnlockReleaseBuffer(leftbuf);
+		if (scankey)
+			pfree(scankey);
+	}
+}
+
+
+/*
+ * _bt_mergepage() -- Perform one leaf-page merge.
+ *
+ * Re-acquires exclusive locks on L, R, and their parent, re-verifies all
+ * preconditions under those exclusive locks, then:
+ *
+ *   1. Saves R's high key and all data tuples to palloc'd memory.
+ *   2. Re-initializes R's page, restoring its opaque header and high key.
+ *   3. Copies all data tuples from L into R, followed by R's original tuples.
+ *   4. Redirects L's downlink in the parent to point to R, then deletes R's
+ *      now-redundant downlink entry from the parent.
+ *   5. Marks R as BTP_MERGED, recording L's block number in R's pd_prune_xid
+ *      field so backward scans can identify the merge group tombstone.
+ *   6. Marks L as BTP_MERGED_AWAY, recording a safemergexid so VACUUM can
+ *      determine when the tombstone page is safe to reclaim.
+ *
+ * Returns true if the merge completed, false if any precondition failed.
+ */
+static bool
+_bt_mergepage(BTMergeState mstate)
+{
+	Relation	rel = mstate.rel;
+	BlockNumber parent_blkno;
+	Buffer		leftbuf,
+				rightbuf,
+				parentbuf = InvalidBuffer;
+	Page		leftpage,
+				rightpage,
+				parentpage;
+	BTPageOpaque leftopaque,
+				rightopaque,
+				popaque;
+	BTStack		stack = mstate.stack;
+	ItemId		itemid;
+	IndexTuple	itup,
+				left_itup,
+				r_hikey = NULL;
+	Size		r_hikey_size = 0,
+				sz;
+	OffsetNumber next_off;
+	int			n_right;
+	IndexTuple *r_tuples = NULL;
+	Size	   *r_sizes = NULL;
+	BTPageOpaqueData saved_opaque;
+	FullTransactionId safemergexid;
+	bool		merged = false;
+	XLogRecPtr	recptr;
+
+	parent_blkno = stack->bts_blkno;
+
+	leftbuf = ReadBuffer(rel, mstate.left_blkno);
+	LockBuffer(leftbuf, BT_WRITE);
+	leftpage = BufferGetPage(leftbuf);
+	leftopaque = BTPageGetOpaque(leftpage);
+	Assert(P_ISLEAF(leftopaque));
+
+	INJECTION_POINT("after_left_lock", NULL);
+
+	rightbuf = ReadBuffer(rel, mstate.right_blkno);
+	LockBuffer(rightbuf, BT_WRITE);
+	rightpage = BufferGetPage(rightbuf);
+	rightopaque = BTPageGetOpaque(rightpage);
+	Assert(P_ISLEAF(rightopaque));
+
+	/* Re-verify left & right leaf pages under exclusive lock. */
+	if (P_ISDELETED(leftopaque) || P_ISHALFDEAD(leftopaque) ||
+		P_ISMERGED(leftopaque) || P_ISMERGEDAWAY(leftopaque) ||
+		P_INCOMPLETE_SPLIT(leftopaque) ||
+		leftopaque->btpo_next != mstate.right_blkno)
+		goto unlock_leaf_bufs;
+
+	if (P_ISDELETED(rightopaque) || P_ISHALFDEAD(rightopaque) ||
+		P_ISMERGED(rightopaque) || P_ISMERGEDAWAY(rightopaque) ||
+		P_INCOMPLETE_SPLIT(rightopaque))
+		goto unlock_leaf_bufs;
+
+	if (!_bt_pages_mergeable(leftpage, rightpage,
+							 mstate.min_threshold, mstate.fillfactor))
+		goto unlock_leaf_bufs;
+
+	/*
+	 * Lock the parent and confirm that downlinks at bts_offset and
+	 * bts_offset+1 still point to L and R respectively.
+	 */
+	parentbuf = ReadBuffer(rel, parent_blkno);
+	LockBuffer(parentbuf, BT_WRITE);
+	parentpage = BufferGetPage(parentbuf);
+	popaque = BTPageGetOpaque(parentpage);
+
+	if (P_ISDELETED(popaque) || P_ISHALFDEAD(popaque) ||
+		popaque->btpo_level != leftopaque->btpo_level + 1)
+		goto unlock_all_bufs;
+
+	next_off = OffsetNumberNext(stack->bts_offset);
+	if (stack->bts_offset < P_FIRSTDATAKEY(popaque) ||
+		next_off > PageGetMaxOffsetNumber(parentpage))
+		goto unlock_all_bufs;
+
+	/* Verify L's downlink */
+	itemid = PageGetItemId(parentpage, stack->bts_offset);
+	if (!ItemIdIsNormal(itemid))
+		goto unlock_all_bufs;
+	left_itup = (IndexTuple) PageGetItem(parentpage, itemid);
+	if (BTreeTupleGetDownLink(left_itup) != mstate.left_blkno)
+		goto unlock_all_bufs;
+
+	/* Verify R's downlink */
+	itemid = PageGetItemId(parentpage, next_off);
+	if (!ItemIdIsNormal(itemid))
+		goto unlock_all_bufs;
+	itup = (IndexTuple) PageGetItem(parentpage, itemid);
+	if (BTreeTupleGetDownLink(itup) != mstate.right_blkno)
+		goto unlock_all_bufs;
+
+	/* Save R's high key (if not rightmost). */
+	if (!P_RIGHTMOST(rightopaque))
+	{
+		ItemId		hikey_id = PageGetItemId(rightpage, P_HIKEY);
+
+		r_hikey_size = ItemIdGetLength(hikey_id);
+		r_hikey = (IndexTuple) palloc(r_hikey_size);
+		memcpy(r_hikey, PageGetItem(rightpage, hikey_id), r_hikey_size);
+	}
+
+	/* Save all of R's data tuples into temporary memory. */
+	{
+		OffsetNumber r_start = P_FIRSTDATAKEY(rightopaque);
+		OffsetNumber r_maxoff = PageGetMaxOffsetNumber(rightpage);
+
+		n_right = (r_maxoff >= r_start) ? (r_maxoff - r_start + 1) : 0;
+		r_tuples = palloc_array(IndexTuple, n_right);
+		r_sizes = palloc_array(Size, n_right);
+
+		for (int i = 0; i < n_right; i++)
+		{
+			itemid = PageGetItemId(rightpage, r_start + i);
+			sz = ItemIdGetLength(itemid);
+			itup = (IndexTuple) PageGetItem(rightpage, itemid);
+			r_tuples[i] = (IndexTuple) palloc(sz);
+			memcpy(r_tuples[i], itup, sz);
+			r_sizes[i] = sz;
+		}
+	}
+
+	safemergexid = ReadNextFullTransactionId();
+
+	/*
+	 * Any failure across the three-buffer update (L becomes a tombstone, R
+	 * absorbs all tuples, parent loses R's downlink) would leave the index in
+	 * an inconsistent state.  Perform all three modifications as an atomic
+	 * unit inside a critical section so that any error panics the server
+	 * rather than aborting with a partially updated index. 
+	 */
+	START_CRIT_SECTION();
+
+	/* Reinitialize R, preserving its opaque header. */
+	saved_opaque = *rightopaque;
+	PageInit(rightpage, BufferGetPageSize(rightbuf), sizeof(BTPageOpaqueData));
+	*BTPageGetOpaque(rightpage) = saved_opaque;
+
+	if (r_hikey != NULL)
+	{
+		if (PageAddItem(rightpage, r_hikey, r_hikey_size,
+						P_HIKEY, false, false) == InvalidOffsetNumber)
+			elog(PANIC, "failed to restore high key to merged page");
+	}
+
+	/* Copy L's data tuples into R. */
+	for (OffsetNumber off = P_FIRSTDATAKEY(leftopaque);
+		 off <= PageGetMaxOffsetNumber(leftpage);
+		 off++)
+	{
+		itemid = PageGetItemId(leftpage, off);
+		sz = ItemIdGetLength(itemid);
+		itup = (IndexTuple) PageGetItem(leftpage, itemid);
+
+		if (PageAddItem(rightpage, itup, sz,
+						InvalidOffsetNumber, false, false) == InvalidOffsetNumber)
+			elog(PANIC, "failed to copy left tuple to merged page");
+	}
+
+	/* Append R's original data tuples after L's. */
+	for (int i = 0; i < n_right; i++)
+	{
+		if (PageAddItem(rightpage, r_tuples[i], r_sizes[i],
+						InvalidOffsetNumber, false, false) == InvalidOffsetNumber)
+			elog(PANIC, "failed to copy right tuple to merged page");
+	}
+
+	/* Redirect L's downlink to R and delete R's downlink entry. */
+	BTreeTupleSetDownLink(left_itup, mstate.right_blkno);
+	PageIndexTupleDelete(parentpage, next_off);
+
+	BTPageSetMerged(rightpage);
+	BTMergedPageSetMABlkno(rightpage, mstate.left_blkno);
+	BTPageSetMergedAway(leftpage, safemergexid);
+
+	MarkBufferDirty(leftbuf);
+	MarkBufferDirty(rightbuf);
+	MarkBufferDirty(parentbuf);
+
+	if(RelationNeedsWAL(rel))
+	{
+		xl_btree_merge xlrec;
+
+		xlrec.left_prev = leftopaque->btpo_prev;
+		xlrec.left_next = mstate.right_blkno;
+		xlrec.poffset = stack->bts_offset;
+		xlrec.safemergexid = safemergexid;
+
+		XLogBeginInsert();
+		XLogRegisterData(&xlrec, SizeOfBtreeMerge);
+
+		XLogRegisterBuffer(0, leftbuf, REGBUF_WILL_INIT);
+		XLogRegisterBuffer(1, rightbuf, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
+		XLogRegisterBuffer(2, parentbuf, REGBUF_STANDARD);
+
+		recptr = XLogInsert(RM_BTREE2_ID, XLOG_BTREE2_MERGE);
+	}
+	else
+		recptr = XLogGetFakeLSN(rel);
+
+	PageSetLSN(leftpage,   recptr);
+    PageSetLSN(rightpage,  recptr);
+    PageSetLSN(parentpage, recptr);
+
+
+	END_CRIT_SECTION();
+
+	/* Free temporary memory. */
+	if (r_hikey != NULL)
+		pfree(r_hikey);
+	for (int i = 0; i < n_right; i++)
+		pfree(r_tuples[i]);
+	pfree(r_tuples);
+	pfree(r_sizes);
+
+	PredicateLockPageCombine(rel, mstate.left_blkno, mstate.right_blkno);
+	merged = true;
+
+unlock_all_bufs:
+	UnlockReleaseBuffer(parentbuf);
+
+unlock_leaf_bufs:
+	UnlockReleaseBuffer(rightbuf);
+	UnlockReleaseBuffer(leftbuf);
+
+	return merged;
+}
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index ff7d2a93948..9c87ad445fa 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -1773,7 +1773,7 @@ _bt_rightsib_halfdeadflag(Relation rel, BlockNumber leafrightsib)
 	opaque = BTPageGetOpaque(page);
 
 	Assert(P_ISLEAF(opaque) && !P_ISDELETED(opaque));
-	result = P_ISHALFDEAD(opaque);
+	result = P_ISHALFDEAD(opaque) || P_ISMERGEDAWAY(opaque);
 	_bt_relbuf(rel, buf);
 
 	return result;
@@ -2569,7 +2569,7 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno,
 	}
 
 	rightsib_is_rightmost = P_RIGHTMOST(opaque);
-	*rightsib_empty = (P_FIRSTDATAKEY(opaque) > PageGetMaxOffsetNumber(page));
+	*rightsib_empty = !P_ISMERGEDAWAY(opaque) && (P_FIRSTDATAKEY(opaque) > PageGetMaxOffsetNumber(page));
 
 	/*
 	 * If we are deleting the next-to-last page on the target's level, then
@@ -3129,3 +3129,83 @@ _bt_pendingfsm_add(BTVacState *vstate,
 	vstate->pendingpages[vstate->npendingpages].safexid = safexid;
 	vstate->npendingpages++;
 }
+
+bool
+_bt_pages_share_parent(Relation rel, BlockNumber left_blkno,
+					   BlockNumber right_blkno, BTScanInsert scankey, BTStack *stack_out)
+{
+	BTStack		stack;
+	Buffer		found_buf = InvalidBuffer;
+	BlockNumber parent_blkno = InvalidBlockNumber;
+	Buffer		parentbuf;
+	Page		parentpage;
+	OffsetNumber maxoff,
+				left_off;
+	bool		found_left = false;
+	IndexTuple	itup;
+	BlockNumber child;
+
+
+	/* Descend the tree to find left's parent. Caller built scankey. */
+	stack = _bt_search(rel, NULL, scankey, &found_buf, BT_READ, true);
+
+	if (BufferIsValid(found_buf))
+		UnlockReleaseBuffer(found_buf);
+
+	if (stack == NULL)
+		return false;
+
+	parent_blkno = stack->bts_blkno;
+
+	if (parent_blkno == InvalidBlockNumber)
+	{
+		_bt_freestack(stack);
+		return false;
+	}
+
+	parentbuf = ReadBuffer(rel, parent_blkno);
+	LockBuffer(parentbuf, BUFFER_LOCK_SHARE);
+	parentpage = BufferGetPage(parentbuf);
+
+	left_off = stack->bts_offset;
+
+	maxoff = PageGetMaxOffsetNumber(parentpage);
+
+	itup = (IndexTuple) PageGetItem(parentpage, PageGetItemId(parentpage, left_off));
+
+	child = ItemPointerGetBlockNumberNoCheck(&itup->t_tid);
+
+	if (child == left_blkno)
+	{
+		found_left = true;
+	}
+	/**
+	 * check if off + 1 in the parent is the right sibling
+	 */
+	if (found_left)
+	{
+		OffsetNumber next_off = OffsetNumberNext(left_off);
+
+		if (next_off <= maxoff)
+		{
+			itup = (IndexTuple) PageGetItem(parentpage, PageGetItemId(parentpage, next_off));
+
+			child = ItemPointerGetBlockNumberNoCheck(&itup->t_tid);
+
+			if (child == right_blkno)
+			{
+				UnlockReleaseBuffer(parentbuf);
+				if (stack_out != NULL)
+					*stack_out = stack;
+				else
+					_bt_freestack(stack);
+				return true;
+			}
+		}
+	}
+
+	UnlockReleaseBuffer(parentbuf);
+	_bt_freestack(stack);
+	return false;
+
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 0abdd7b49f5..99da9dee387 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -21,6 +21,8 @@
 #include "access/nbtree.h"
 #include "access/relscan.h"
 #include "access/stratnum.h"
+#include "access/nbtxlog.h"
+#include "access/xloginsert.h"
 #include "commands/progress.h"
 #include "commands/vacuum.h"
 #include "nodes/execnodes.h"
@@ -37,6 +39,8 @@
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
 #include "utils/wait_event.h"
+#include "utils/injection_point.h"
+
 
 
 /*
@@ -374,6 +378,12 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	 */
 	so->currTuples = so->markTuples = NULL;
 
+	/* Initialize merge fields */
+	so->skipMergeRecovery = false;
+	so->needMergeRecovery = false;
+	so->nSavedMergeTids = 0;
+	so->mergedAwayBlkno = InvalidBlockNumber;
+
 	scan->xs_itupdesc = RelationGetDescr(rel);
 
 	scan->opaque = so;
@@ -427,6 +437,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
+	so->skipMergeRecovery = false;
+	so->needMergeRecovery = false;
+	so->nSavedMergeTids = 0;
+	so->mergedAwayBlkno = InvalidBlockNumber;
+
 	/*
 	 * Allocate tuple workspace arrays, if needed for an index-only scan and
 	 * not already done in a previous rescan call.  To save on palloc
@@ -1518,6 +1533,252 @@ backtrack:
 		 * pages_deleted stats in all cases (barring corruption)
 		 */
 	}
+	else if (P_ISMERGEDAWAY(opaque))
+	{
+		FullTransactionId safemergexid = BTMergedAwayGetSafeXid(page);
+
+		if (GlobalVisCheckRemovableFullXid(heaprel, safemergexid))
+		{
+			BlockNumber tail_blkno;
+			BlockNumber curr_blkno = opaque->btpo_next;
+			Buffer		curr_buf;
+			BTPageOpaque curr_opaque;
+			Buffer		next_buf;
+			BTPageOpaque next_opaque;
+			BlockNumber next_blkno;
+
+			BlockNumber bwd_blkno;
+			BlockNumber right_anchor;
+			Buffer		bwd_buf;
+			Page		bwd_page,
+						curr_page;
+			BTPageOpaque bwd_opaque;
+			BlockNumber nextblk;
+			BTPageOpaqueData saved_opaque;
+			IndexTupleData trunctuple;
+			XLogRecPtr	recptr;
+
+			/*
+			 * We release the MA page lock here because the forward and
+			 * backward walks acquire write locks on M pages.  Holding the MA
+			 * read lock across multiple buffer lock acquisitions would
+			 * violate PostgreSQL's lock ordering rules and risk deadlock. The
+			 * pin on buf is retained throughout, preventing the MA page from
+			 * being recycled.
+			 */
+			LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+
+			/* Walk Forward to find the tail using lock coupling. */
+			curr_buf = _bt_getbuf(rel, curr_blkno, BT_READ);
+			curr_page = BufferGetPage(curr_buf);
+			curr_opaque = BTPageGetOpaque(curr_page);
+
+			/*
+			 * If the last VACUUM run cleared all M page flags but stopped
+			 * before clearing the MA page flag, skip straight to finalizing
+			 * the tombstone.
+			 */
+			if (!BTPageIsMergedMember(curr_opaque, curr_page, blkno))
+			{
+				_bt_relbuf(rel, curr_buf);
+				goto finalize_ma;
+			}
+
+			while (true)
+			{
+				next_blkno = curr_opaque->btpo_next;
+
+				if (next_blkno == P_NONE)
+				{
+					tail_blkno = curr_blkno;
+					right_anchor = P_NONE;
+					_bt_relbuf(rel, curr_buf);
+					break;
+				}
+
+				next_buf = _bt_getbuf(rel, next_blkno, BT_READ);
+				next_opaque = BTPageGetOpaque(BufferGetPage(next_buf));
+
+				/* End of the merge group reached; stop the forward walk. */
+				if (!BTPageIsMergedMember(next_opaque, BufferGetPage(next_buf), blkno))
+				{
+					tail_blkno = curr_blkno;
+					right_anchor = next_blkno;
+					_bt_relbuf(rel, curr_buf);
+					_bt_relbuf(rel, next_buf);
+					break;
+				}
+
+				_bt_relbuf(rel, curr_buf);
+				curr_blkno = next_blkno;
+				curr_buf = next_buf;
+				curr_opaque = next_opaque;
+			}
+
+			/*
+			 * Walk Backward to clear M flags (Stop before hitting the MA
+			 * tombstone!)
+			 */
+			bwd_blkno = tail_blkno;
+
+			while (bwd_blkno != blkno)
+			{
+				CHECK_FOR_INTERRUPTS();
+
+				bwd_buf = _bt_getbuf(rel, bwd_blkno, BT_WRITE);
+				bwd_page = BufferGetPage(bwd_buf);
+				bwd_opaque = BTPageGetOpaque(bwd_page);
+
+				while (bwd_opaque->btpo_next != right_anchor)
+				{
+					nextblk = bwd_opaque->btpo_next;
+					_bt_relbuf(rel, bwd_buf);
+
+					CHECK_FOR_INTERRUPTS();
+
+					bwd_buf = _bt_getbuf(rel, nextblk, BT_WRITE);
+					bwd_page = BufferGetPage(bwd_buf);
+					bwd_opaque = BTPageGetOpaque(bwd_page);
+
+					if (!BTPageIsMergedMember(bwd_opaque, bwd_page, blkno))
+					{
+						_bt_relbuf(rel, bwd_buf);
+						/* Lock MA page again before skiping cleanup */
+						LockBuffer(buf, BUFFER_LOCK_SHARE);
+						ereport(WARNING,
+								(errmsg("merged-away page %u: unexpected page state near block %u, deferring remaining cleanup to a future VACUUM",
+										blkno, bwd_blkno)));
+
+						goto skip_merge_cleanup;
+					}
+				}
+
+				/*
+				 * If we encounter a page from a different merge group,
+				 * something is wrong; skip the remaining cleanup and defer to
+				 * a future VACUUM.  This case is very unlikely because: -
+				 * When a page splits, the new page also inherits the M flag.
+				 * - No other VACUUM process runs concurrently on the same
+				 * index, so no other process can reset any page in between.
+				 */
+				if (!BTPageIsMergedMember(bwd_opaque, bwd_page, blkno))
+				{
+					_bt_relbuf(rel, bwd_buf);
+
+					/* Lock MA page again before skiping cleanup */
+					LockBuffer(buf, BUFFER_LOCK_SHARE);
+					ereport(WARNING,
+							(errmsg("merged-away page %u: unexpected page state near block %u, deferring remaining cleanup to a future VACUUM",
+									blkno, bwd_blkno)));
+
+					goto skip_merge_cleanup;
+				}
+
+				START_CRIT_SECTION();
+
+				bwd_opaque->btpo_flags &= ~(BTP_MERGED);
+				BTMergedPageClearMABlkno(bwd_page);
+				MarkBufferDirty(bwd_buf);
+
+				if(RelationNeedsWAL(rel))
+				{
+					xl_btree_clear_m xlrec;
+					xlrec.safemergexid = safemergexid;
+					xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
+
+					XLogBeginInsert();
+
+					XLogRegisterData(&xlrec, SizeOfBtreeClearM);
+
+					XLogRegisterBuffer(0, bwd_buf, REGBUF_STANDARD);
+
+					recptr = XLogInsert(RM_BTREE2_ID, XLOG_BTREE2_CLEAR_MERGE_FLAG);
+				}
+				else
+					recptr = XLogGetFakeLSN(rel);
+
+				PageSetLSN(bwd_page,   recptr);
+
+				END_CRIT_SECTION();
+
+				right_anchor = BufferGetBlockNumber(bwd_buf);
+				bwd_blkno = bwd_opaque->btpo_prev;
+				_bt_relbuf(rel, bwd_buf);
+			}
+
+	finalize_ma:
+
+			/* We are back at the tombstone(MA) to make it HD */
+
+			/* Upgrade our read lock to a write lock while keeping the pin! */
+			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+			if (!P_ISMERGEDAWAY(opaque) ||
+				!FullTransactionIdEquals(BTMergedAwayGetSafeXid(page), safemergexid))
+			{
+				/*
+				 * Downgrade from exclusive back to share lock before jumping
+				 * to skip_merge_cleanup.  The pin is still held, so the brief
+				 * unlock window is safe.  btvacuumpage and its caller expect
+				 * buf to exit with a lock held.
+				 */
+				LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+				LockBuffer(buf, BUFFER_LOCK_SHARE);
+				ereport(WARNING,
+						(errmsg("merged-away page %u changed concurrently, skipping cleanup",
+								blkno)));
+				goto skip_merge_cleanup;
+			}
+
+			/* _bt_pagedel() stage 2 requires a high key to exist on the page. */
+
+			saved_opaque = *opaque;
+
+			MemSet(&trunctuple, 0, sizeof(IndexTupleData));
+			trunctuple.t_info = sizeof(IndexTupleData);
+			BTreeTupleSetTopParent(&trunctuple, InvalidBlockNumber);
+
+			START_CRIT_SECTION();
+
+			PageInit(page, BufferGetPageSize(buf), sizeof(BTPageOpaqueData));
+
+			if (PageAddItem(page, (IndexTuple) &trunctuple, IndexTupleSize(&trunctuple),
+							P_HIKEY, false, false) == InvalidOffsetNumber)
+				elog(PANIC, "could not add dummy high key to half-dead page");
+
+			opaque = BTPageGetOpaque(page);
+			*opaque = saved_opaque;
+			opaque->btpo_flags &= ~(BTP_MERGED_AWAY | BTP_HAS_FULLXID);
+			opaque->btpo_flags |= BTP_HALF_DEAD;
+			MarkBufferDirty(buf);
+
+
+			if(RelationNeedsWAL(rel))
+			{
+				xl_btree_mark_ma_hd xlrec;
+				xlrec.left_prev = opaque->btpo_prev;
+				xlrec.left_next = opaque->btpo_next;
+				xlrec.safemergexid = safemergexid;
+				xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
+
+				XLogBeginInsert();
+
+				XLogRegisterData(&xlrec, SizeOfBtreeMarkMaHd);
+
+				XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT);
+
+				recptr = XLogInsert(RM_BTREE2_ID, XLOG_BTREE2_MERGE_MARK_HALFDEAD);
+			}
+			else
+				recptr = XLogGetFakeLSN(rel);
+
+			PageSetLSN(page,   recptr);
+
+			END_CRIT_SECTION();
+
+			attempt_pagedel = true;
+		}
+	}
 	else if (P_ISLEAF(opaque))
 	{
 		OffsetNumber deletable[MaxIndexTuplesPerPage];
@@ -1695,6 +1956,8 @@ backtrack:
 		Assert(!attempt_pagedel || nhtidslive == 0);
 	}
 
+skip_merge_cleanup:
+
 	if (attempt_pagedel)
 	{
 		MemoryContext oldcontext;
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 5964bc9195e..2cfd65057e6 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -26,6 +26,7 @@
 #include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/injection_point.h"
 
 
 static inline void _bt_drop_lock_and_maybe_pin(Relation rel, BTScanOpaque so);
@@ -45,7 +46,11 @@ static bool _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 static Buffer _bt_lock_and_validate_left(Relation rel, BlockNumber *blkno,
 										 BlockNumber lastcurrblkno);
 static bool _bt_endpoint(IndexScanDesc scan, ScanDirection dir);
-
+static void _bt_removeduplicates(IndexScanDesc scan);
+static void _bt_find_merge_tail(IndexScanDesc scan, BlockNumber m_blkno, BlockNumber *blkno,
+								BlockNumber *lastcurrblkno);
+static void _bt_copylastreadpagedata(IndexScanDesc scan);
+static int	compare(const void *a, const void *b);
 
 /*
  *	_bt_drop_lock_and_maybe_pin()
@@ -185,6 +190,12 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP,
 		*bufP = _bt_relandgetbuf(rel, *bufP, child, page_access);
 
 		/* okay, all set to move down a level */
+		if (strcmp(RelationGetRelationName(rel), "merge_test_idx") == 0 && opaque->btpo_level == 1 && access == BT_WRITE)
+		{
+			_bt_relbuf(rel, *bufP);
+			INJECTION_POINT("before_leaf_level", NULL);
+			*bufP = _bt_getbuf(rel, child, page_access);
+		}
 	}
 
 	/*
@@ -306,7 +317,7 @@ _bt_moveright(Relation rel,
 			continue;
 		}
 
-		if (P_IGNORE(opaque) || _bt_compare(rel, key, page, P_HIKEY) >= cmpval)
+		if (P_IGNORE(opaque) || P_ISMERGEDAWAY(opaque) || _bt_compare(rel, key, page, P_HIKEY) >= cmpval)
 		{
 			/* step right one page */
 			buf = _bt_relandgetbuf(rel, buf, opaque->btpo_next, access);
@@ -1755,6 +1766,26 @@ _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, ScanDirection dir)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
+	/*
+	 * If the starting page has BTP_MERGED, we descended directly into a merge
+	 * group (rather than stepping from a MERGED_AWAY tombstone).  Set
+	 * skipMergeRecovery so the scan loop knows no deduplication is needed for
+	 * the subsequent MERGED pages in this group.
+	 *
+	 * so->currPos.buf is already pinned and locked on entry, so we can safely
+	 * read the page opaque here without any extra locking.
+	 */
+	{
+		Page		page = BufferGetPage(so->currPos.buf);
+		BTPageOpaque opaque = BTPageGetOpaque(page);
+
+		if (P_ISMERGED(opaque))
+		{
+			so->skipMergeRecovery = true;
+			so->mergedAwayBlkno = BTMergedPageGetMABlkno(page);
+		}
+	}
+
 	so->numKilled = 0;			/* just paranoia */
 	so->markItemIndex = -1;		/* ditto */
 
@@ -1849,11 +1880,23 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	bool		needMergeRecoverWalk = false;
+	BlockNumber m_blkno = InvalidBlockNumber;
 
 	Assert(so->currPos.currPage == lastcurrblkno || seized);
 	Assert(!(blkno == P_NONE && seized));
 	Assert(!BTScanPosIsPinned(so->currPos));
 
+	if (strcmp(RelationGetRelationName(rel), "merge_test_idx") == 0 && ScanDirectionIsForward(dir) && blkno == 4)
+	{
+		INJECTION_POINT("before_read_next_page", NULL);
+	}
+
+	if (strcmp(RelationGetRelationName(rel), "merge_test_idx") == 0 && ScanDirectionIsBackward(dir) && blkno == 2)
+	{
+		INJECTION_POINT("before_read_prev_page", NULL);
+	}
+
 	/*
 	 * Remember that the scan already read lastcurrblkno, a page to the left
 	 * of blkno (or remember reading a page to the right, for backwards scans)
@@ -1868,6 +1911,8 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 		Page		page;
 		BTPageOpaque opaque;
 
+		needMergeRecoverWalk = false;
+
 		if (blkno == P_NONE ||
 			(ScanDirectionIsForward(dir) ?
 			 !so->currPos.moreRight : !so->currPos.moreLeft))
@@ -1913,8 +1958,21 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 		page = BufferGetPage(so->currPos.buf);
 		opaque = BTPageGetOpaque(page);
 		lastcurrblkno = blkno;
-		if (likely(!P_IGNORE(opaque)))
+
+
+		if (likely(!P_IGNORE(opaque) && !P_ISMERGEDAWAY(opaque) && !P_ISMERGED(opaque)))
 		{
+			/*
+			 * We landed on a normal page. If we were in a merge group, we
+			 * have now exited it. Clear the merge recovery flags so we are
+			 * ready for the next merge group.
+			 */
+			if (so->needMergeRecovery || so->skipMergeRecovery)
+			{
+				so->needMergeRecovery = false;
+				so->skipMergeRecovery = false;
+			}
+
 			/* see if there are any matches on this page */
 			if (ScanDirectionIsForward(dir))
 			{
@@ -1931,7 +1989,7 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 				blkno = so->currPos.prevPage;
 			}
 		}
-		else
+		else if (unlikely(P_IGNORE(opaque)))
 		{
 			/* _bt_readpage not called, so do all this for ourselves */
 			if (ScanDirectionIsForward(dir))
@@ -1941,10 +1999,181 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 			if (scan->parallel_scan != NULL)
 				_bt_parallel_release(scan, blkno, lastcurrblkno);
 		}
+		else if (P_ISMERGEDAWAY(opaque))
+		{
+			if (ScanDirectionIsForward(dir))
+			{
+				/*
+				 * Save state indicating we passed a merged away page and
+				 * proceed to the next page
+				 */
+				so->skipMergeRecovery = true;
+				so->mergedAwayBlkno = blkno;
+				blkno = opaque->btpo_next;
+			}
+			else
+			{
+				if (so->mergedAwayBlkno != blkno)
+				{
+					so->skipMergeRecovery = false;
+					so->needMergeRecovery = false;
+				}
+
+				/*
+				 * BACKWARD SCAN: Tombstone (BTP_MERGED_AWAY)
+				 */
+				if (so->skipMergeRecovery || so->needMergeRecovery)
+				{
+					/*
+					 * We either already saw the merged page
+					 * (skipMergeRecovery), or we just finished walking
+					 * backward through the recovery group
+					 * (needMergeRecovery). In either case, we safely skip
+					 * this tombstone and step left.
+					 */
+					blkno = opaque->btpo_prev;
+
+					/* Clear the states since we are exiting the merge group */
+					so->skipMergeRecovery = false;
+					so->needMergeRecovery = false;
+				}
+				else
+				{
+					/*
+					 * We hit the tombstone but haven't seen the merged page
+					 * yet! This means the merge happened right after we read
+					 * the left page. We must enter recovery mode to find the
+					 * merged tuples.
+					 */
+					so->needMergeRecovery = true;
+
+					/*
+					 * 1- Save the TIDs we already read to filter them out
+					 * later
+					 */
+					_bt_copylastreadpagedata(scan);
+
+					/*
+					 * 2- Trigger the rightward walk at the end of the loop to
+					 * find the tail page
+					 */
+					needMergeRecoverWalk = true;
+					m_blkno = opaque->btpo_next;	/* The start of the merged
+													 * group */
+				}
+			}
+		}
+		else if (P_ISMERGED(opaque))
+		{
+			if (ScanDirectionIsForward(dir))
+			{
+				if (BTMergedPageGetMABlkno(page) != so->mergedAwayBlkno)
+				{
+					so->skipMergeRecovery = false;
+					so->mergedAwayBlkno = BTMergedPageGetMABlkno(page);
+				}
+
+				/*
+				 * Case 1: We passed the BTP_MERGED_AWAY page already and that
+				 * merged away page we have its blkno saved (will do this part
+				 * later). We read this page as a normal page.
+				 */
+				if (so->skipMergeRecovery)
+				{
+					if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
+						break;
+					blkno = so->currPos.nextPage;
+				}
+				else
+				{
+					/*
+					 * FWD SCAN: Recovery Mode We read the left page before it
+					 * was merged.
+					 */
+					if (!so->needMergeRecovery)
+					{
+						/*
+						 * First time hitting the merged group! Save L's items
+						 * to filter them out of R and any split descendants.
+						 */
+						_bt_copylastreadpagedata(scan);
+						so->needMergeRecovery = true;
+					}
+
+					if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
+					{
+						_bt_removeduplicates(scan);
+
+						/*
+						 * Only break to return if we still have unfiltered
+						 * tuples
+						 */
+						if (so->currPos.lastItem >= so->currPos.firstItem)
+							break;
+					}
+					blkno = so->currPos.nextPage;
+				}
+			}
+			else
+			{
+				so->mergedAwayBlkno = BTMergedPageGetMABlkno(page);
+
+				/*
+				 * BACKWARD SCAN: Merged Page (BTP_MERGED)
+				 */
+				if (so->needMergeRecovery)
+				{
+					/*
+					 * We are currently walking backward through the recovery
+					 * group. Read the page, but filter out tuples we already
+					 * saw.
+					 */
+					if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
+					{
+						_bt_removeduplicates(scan);
+
+						/*
+						 * Only break to return if we still have unfiltered
+						 * tuples
+						 */
+						if (so->currPos.lastItem >= so->currPos.firstItem)
+							break;
+					}
+
+					blkno = so->currPos.prevPage;	/* Step left to the next
+													 * page in the group */
+				}
+				else
+				{
+					/*
+					 * Normal backward scan. We hit a merged page directly.
+					 * Set the skip flag so when we step left onto the
+					 * tombstone, we skip it.
+					 */
+					so->skipMergeRecovery = true;
+
+					if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
+						break;
+
+					blkno = so->currPos.prevPage;
+				}
+			}
+		}
 
 		/* no matching tuples on this page */
 		_bt_relbuf(rel, so->currPos.buf);
 		seized = false;			/* released by _bt_readpage (or by us) */
+
+		if (needMergeRecoverWalk)
+		{
+			_bt_find_merge_tail(scan, m_blkno, &blkno, &lastcurrblkno);
+
+			/*
+			 * After this call the loop will continue to read blkno page * so
+			 * we now have to how these MERGED pages are going to be read
+			 */
+			needMergeRecoverWalk = false;
+		}
 	}
 
 	/*
@@ -1955,9 +2184,219 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 	Assert(BTScanPosIsPinned(so->currPos));
 	_bt_drop_lock_and_maybe_pin(rel, so);
 
+
 	return true;
 }
 
+
+/*
+ *	_bt_find_merge_tail() -- Find the tail of a MERGED page group for backward scan recovery.
+ *
+ * During a backward scan, when the scan discovers it needs merge recovery
+ * (needMergeRecovery), it must walk forward from the first MERGED page (R)
+ * to locate the rightmost page in the merge group (the "tail").  This is
+ * needed so the backward scan can restart from the tail and walk backward
+ * through the full merge group, deduplicating against savedMergeTids.
+ *
+ * m_blkno is the block number of the first MERGED page (the immediate right
+ * sibling of the MERGED_AWAY tombstone).  On return, *blkno is set to the
+ * tail of the merge group (the last consecutive MERGED page), and
+ * *lastcurrblkno is set to its right neighbor (the first non-MERGED page),
+ * which is used as the right anchor for split validation during the
+ * subsequent backward scan.
+ *
+ * Uses lock-coupling (acquire right, release left) to traverse the chain
+ * safely under concurrent activity.
+ */
+static void
+_bt_find_merge_tail(IndexScanDesc scan, BlockNumber m_blkno, BlockNumber *blkno,
+					BlockNumber *lastcurrblkno)
+{
+	Relation	rel = scan->indexRelation;
+	BlockNumber r_blkno,
+				l_blkno;
+	Buffer		r_buf,
+				l_buf;
+	Page		r_page,
+				l_page;
+	BTPageOpaque r_opaque,
+				l_opaque;
+
+	/* L <--> R(r_blkno) <--> X <--> Y(tail) <--> Z (first non MERGED page) */
+
+	/*
+	 * Step 1: Initialize the left page. m_blkno is the first merged page (R)
+	 * we got from the tombstone's btpo_next.
+	 */
+	l_blkno = m_blkno;
+	l_buf = _bt_getbuf(rel, l_blkno, BT_READ);
+	l_page = BufferGetPage(l_buf);
+	l_opaque = BTPageGetOpaque(l_page);
+
+	Assert(P_ISMERGED(l_opaque));
+
+	/*
+	 * Step 2: Lock-couple rightward to find the boundary. Loop till we find a
+	 * non-MERGED page or the rightmost edge.
+	 */
+	for (;;)
+	{
+		r_blkno = l_opaque->btpo_next;
+
+		/*
+		 * Check if the merged group extends to the rightmost edge of the
+		 * index
+		 */
+		if (r_blkno == P_NONE)
+		{
+			*blkno = l_blkno;
+			*lastcurrblkno = P_NONE;	/* No page to the right, so no right
+										 * anchor */
+			_bt_relbuf(rel, l_buf);
+			break;
+		}
+
+		/* Lock the right page */
+		r_buf = _bt_getbuf(rel, r_blkno, BT_READ);
+		r_page = BufferGetPage(r_buf);
+		r_opaque = BTPageGetOpaque(r_page);
+
+		/* Check if we found the boundary (the first non-merged page) */
+		if (!P_ISMERGED(r_opaque))
+		{
+			*blkno = l_blkno;	/* The tail of the merged group */
+			*lastcurrblkno = r_blkno;	/* The right anchor for validation */
+
+			/* We have our boundary. Unlock both pages before returning. */
+			_bt_relbuf(rel, l_buf);
+			_bt_relbuf(rel, r_buf);
+
+			break;
+		}
+
+		/* The right page is also merged, so shift right (lock-coupling) */
+
+		/* Unlock the old left page */
+		_bt_relbuf(rel, l_buf);
+
+		/* Make the right page the new left page for the next iteration */
+		l_blkno = r_blkno;
+		l_buf = r_buf;
+		l_page = r_page;
+		l_opaque = r_opaque;
+	}
+}
+
+
+/*
+ *	_bt_copylastreadpagedata() -- Snapshot heap TIDs from the last-read page
+ *								 into savedMergeTids before they are overwritten.
+ *
+ * currPos.items is overwritten by every call to _bt_readpage.  When merge
+ * recovery is triggered (either forward or backward), we must preserve the
+ * TIDs that were already returned (or about to be returned) from the
+ * MERGED_AWAY page so that _bt_removeduplicates can filter them out of the
+ * MERGED pages' contents later.
+ *
+ * Copies every heapTid from currPos.items[firstItem..lastItem] into
+ * so->savedMergeTids and sets so->nSavedMergeTids accordingly.  The array
+ * is sorted in place (using ItemPointerCompare) so that _bt_removeduplicates
+ * can use bsearch() for O(log n) lookups.
+ *
+ * Must be called while currPos still reflects the page whose TIDs we want
+ * to save, i.e. before the next _bt_readpage call.
+ */
+static void
+_bt_copylastreadpagedata(IndexScanDesc scan)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			firstItem = so->currPos.firstItem;
+	int			lastItem = so->currPos.lastItem;
+	ItemPointerData item;
+
+	so->nSavedMergeTids = 0;
+
+	for (int i = firstItem; i <= lastItem; i++)
+	{
+		item = so->currPos.items[i].heapTid;
+		so->savedMergeTids[so->nSavedMergeTids++] = item;
+	}
+
+	if (so->nSavedMergeTids > 1)
+		qsort(so->savedMergeTids, so->nSavedMergeTids, sizeof(ItemPointerData), compare);
+}
+
+
+/*
+ *	compare() -- ItemPointerData comparator for qsort() and bsearch().
+ *
+ * Used by _bt_copylastreadpagedata to sort savedMergeTids and by
+ * _bt_removeduplicates to binary-search within that sorted array.
+ */
+static int
+compare(const void *a, const void *b)
+{
+	return ItemPointerCompare((ItemPointer) a, (ItemPointer) b);
+}
+
+/*
+ *	_bt_removeduplicates() -- Filter out already-seen TIDs from the current page
+ *							  during merge-group recovery.
+ *
+ * After a concurrent page merge is detected, the MERGED page(s) contain a
+ * superset of the tuples from both the original left (MERGED_AWAY) and right
+ * pages.  Any TID that was already returned to the caller from the
+ * MERGED_AWAY page exists in so->savedMergeTids and must be removed from
+ * currPos.items to prevent duplicates being returned.
+ *
+ * Iterates over currPos.items[firstItem..lastItem] and compacts the array
+ * in-place, retaining only items whose heapTid is NOT found in
+ * savedMergeTids (which must already be sorted by _bt_copylastreadpagedata).
+ * Updates currPos.lastItem and resets currPos.itemIndex to the correct
+ * boundary for the current scan direction.
+ *
+ * Must be called immediately after _bt_readpage on each MERGED page during
+ * recovery, before any items are returned to the caller.
+ */
+static void
+_bt_removeduplicates(IndexScanDesc scan)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			firstItem = so->currPos.firstItem;
+	int			lastItem = so->currPos.lastItem;
+	int			dest = firstItem;
+	ItemPointerData *item,
+			   *found;
+
+	/* Filter out items whose heap TIDs are in mergedAwayTids list */
+	for (int i = firstItem; i <= lastItem; i++)
+	{
+		item = &so->currPos.items[i].heapTid;
+
+		found = (ItemPointerData *) bsearch(item, so->savedMergeTids, so->nSavedMergeTids, sizeof(ItemPointerData), compare);
+
+		if (found)
+		{
+			/* Skip duplicate item */
+			continue;
+		}
+
+		/* Retain non duplicate item */
+		so->currPos.items[dest] = so->currPos.items[i];
+		dest++;
+	}
+	so->currPos.lastItem = dest - 1;
+
+	/*
+	 * Update itemIndex to point to the correct boundary for the scan
+	 * direction
+	 */
+	if (ScanDirectionIsForward(so->currPos.dir))
+		so->currPos.itemIndex = so->currPos.firstItem;
+	else
+		so->currPos.itemIndex = so->currPos.lastItem;
+}
+
 /*
  * _bt_lock_and_validate_left() -- lock caller's left sibling blkno,
  * recovering from concurrent page splits/page deletions when necessary
@@ -2150,7 +2589,7 @@ _bt_get_endpoint(Relation rel, uint32 level, bool rightmost)
 		 * right if needed to get to it (this could happen if the page split
 		 * since we obtained a pointer to it).
 		 */
-		while (P_IGNORE(opaque) ||
+		while (P_IGNORE(opaque) || P_ISMERGEDAWAY(opaque) ||
 			   (rightmost && !P_RIGHTMOST(opaque)))
 		{
 			blkno = opaque->btpo_next;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 014faa1622f..23950f949e0 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -967,7 +967,7 @@ _bt_check_natts(Relation rel, bool heapkeyspace, Page page, OffsetNumber offnum)
 	 * We cannot reliably test a deleted or half-dead page, since they have
 	 * dummy high keys
 	 */
-	if (P_IGNORE(opaque))
+	if (P_IGNORE(opaque) || P_ISMERGEDAWAY(opaque))
 		return true;
 
 	Assert(offnum >= FirstOffsetNumber &&
diff --git a/src/backend/access/nbtree/nbtxlog.c b/src/backend/access/nbtree/nbtxlog.c
index dff7d286fc8..943b37b3269 100644
--- a/src/backend/access/nbtree/nbtxlog.c
+++ b/src/backend/access/nbtree/nbtxlog.c
@@ -258,6 +258,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record)
 	BlockNumber origpagenumber;
 	BlockNumber rightpagenumber;
 	BlockNumber spagenumber;
+	BlockNumber	merge_ma_blkno = xlrec->merged_ma_blkno;
 
 	XLogRecGetBlockTag(record, 0, NULL, NULL, &origpagenumber);
 	XLogRecGetBlockTag(record, 1, NULL, NULL, &rightpagenumber);
@@ -294,6 +295,11 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record)
 	ropaque->btpo_flags = isleaf ? BTP_LEAF : 0;
 	ropaque->btpo_cycleid = 0;
 
+	if(merge_ma_blkno != InvalidBlockNumber){
+		ropaque->btpo_flags |= BTP_MERGED;
+		BTMergedPageSetMABlkno(rpage, merge_ma_blkno);
+	}
+
 	_bt_restore_page(rpage, datapos, datalen);
 
 	PageSetLSN(rpage, lsn);
@@ -415,6 +421,12 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record)
 		oopaque->btpo_flags = BTP_INCOMPLETE_SPLIT;
 		if (isleaf)
 			oopaque->btpo_flags |= BTP_LEAF;
+
+		if(merge_ma_blkno != InvalidBlockNumber){
+			oopaque->btpo_flags |= BTP_MERGED;
+			BTMergedPageSetMABlkno(origpage, merge_ma_blkno);
+		}
+
 		oopaque->btpo_next = rightpagenumber;
 		oopaque->btpo_cycleid = 0;
 
@@ -487,6 +499,13 @@ btree_xlog_dedup(XLogReaderState *record)
 		maxoff = PageGetMaxOffsetNumber(page);
 		newpage = PageGetTempPageCopySpecial(page);
 
+		if (xlrec->merged_ma_blkno != InvalidBlockNumber)
+		{
+			BTPageOpaque nopaque = BTPageGetOpaque(newpage);
+			nopaque->btpo_flags |= BTP_MERGED;
+			BTMergedPageSetMABlkno(newpage, xlrec->merged_ma_blkno);
+		}
+
 		if (!P_RIGHTMOST(opaque))
 		{
 			ItemId		itemid = PageGetItemId(page, P_HIKEY);
@@ -1000,6 +1019,157 @@ btree_xlog_reuse_page(XLogReaderState *record)
 												   xlrec->locator);
 }
 
+static void
+btree_xlog_merge_page(XLogReaderState *record)
+{
+	xl_btree_merge *xlrec = (xl_btree_merge *) XLogRecGetData(record);
+	XLogRecPtr	lsn = record->EndRecPtr;
+	Buffer		buf;
+	Page		page;
+	BTPageOpaque pageop;
+
+	
+	/* parent page */
+	if(XLogReadBufferForRedo(record, 2, &buf) == BLK_NEEDS_REDO){
+		OffsetNumber poffset;
+		ItemId		itemid;
+		IndexTuple	itup;
+		OffsetNumber nextoffset;
+		BlockNumber rightsib;
+
+		page = BufferGetPage(buf);
+		pageop = BTPageGetOpaque(page);
+
+		poffset = xlrec->poffset;
+		nextoffset = OffsetNumberNext(poffset);
+		itemid = PageGetItemId(page, nextoffset);
+		itup = (IndexTuple) PageGetItem(page, itemid);
+		rightsib = BTreeTupleGetDownLink(itup);
+
+		rightsib = BTreeTupleGetDownLink(itup);
+		itemid = PageGetItemId(page, poffset);
+		itup = (IndexTuple) PageGetItem(page, itemid);
+
+		BTreeTupleSetDownLink(itup, rightsib);
+		nextoffset = OffsetNumberNext(poffset);
+		PageIndexTupleDelete(page, nextoffset);
+		PageSetLSN(page, lsn);
+		MarkBufferDirty(buf);
+	}
+	if (BufferIsValid(buf))
+    	UnlockReleaseBuffer(buf);
+	
+	/* Reconstruct	left (MA) page from scratch */
+	buf = XLogInitBufferForRedo(record, 0);
+	page = BufferGetPage(buf);
+
+	_bt_pageinit(page, BufferGetPageSize(buf));
+	pageop = BTPageGetOpaque(page);
+	pageop->btpo_prev = xlrec->left_prev;
+	pageop->btpo_next = xlrec->left_next;
+	pageop->btpo_level = 0;
+	pageop->btpo_flags =  BTP_LEAF;
+	pageop->btpo_cycleid = 0;
+
+	BTPageSetMergedAway(page, xlrec->safemergexid);
+	PageSetLSN(page, lsn);
+	MarkBufferDirty(buf);
+	UnlockReleaseBuffer(buf);
+
+	/* right (M) page */
+	XLogReadBufferForRedo(record, 1, &buf);
+
+	if (BufferIsValid(buf))
+    	UnlockReleaseBuffer(buf);
+
+}
+static void
+btree_xlog_clear_m_flag(XLogReaderState *record){
+	xl_btree_clear_m *xlrec = (xl_btree_clear_m *) XLogRecGetData(record);
+	XLogRecPtr	lsn = record->EndRecPtr;
+	Buffer		buf;
+	Page		page;
+	BTPageOpaque pageop;
+
+	/*
+	 * If we have any conflict processing to do, it must happen before we
+	 * update the page
+	 */
+	if (InHotStandby)
+	{
+		RelFileLocator rlocator;
+	
+		XLogRecGetBlockTag(record, 0, &rlocator, NULL, NULL);
+	
+		ResolveRecoveryConflictWithSnapshotFullXid(
+			xlrec->safemergexid,
+			xlrec->isCatalogRel,
+			rlocator);
+	}
+
+	if(XLogReadBufferForRedo(record, 0, &buf) == BLK_NEEDS_REDO)
+	{
+		page = BufferGetPage(buf);
+		pageop = BTPageGetOpaque(page);
+		BTMergedPageClearMABlkno(page);
+		pageop->btpo_flags &= ~BTP_MERGED;
+		PageSetLSN(page, lsn);
+		MarkBufferDirty(buf);
+	}
+	if (BufferIsValid(buf))
+    	UnlockReleaseBuffer(buf);
+}
+
+static void
+btree_xlog_mark_ma_hd(XLogReaderState *record){
+	xl_btree_mark_ma_hd *xlrec = (xl_btree_mark_ma_hd *) XLogRecGetData(record);
+	XLogRecPtr	lsn = record->EndRecPtr;
+	Buffer		buf;
+	Page		page;
+	BTPageOpaque pageop;
+	IndexTupleData trunctuple;
+
+	/*
+	 * If we have any conflict processing to do, it must happen before we
+	 * update the page
+	 */
+	if (InHotStandby)
+	{
+		RelFileLocator rlocator;
+	
+		XLogRecGetBlockTag(record, 0, &rlocator, NULL, NULL);
+	
+		ResolveRecoveryConflictWithSnapshotFullXid(
+			xlrec->safemergexid,
+			xlrec->isCatalogRel,
+			rlocator);
+	}
+
+    /* Rewrite the MA page as a halfdead page */
+	buf = XLogInitBufferForRedo(record, 0);
+	page = BufferGetPage(buf);
+
+	_bt_pageinit(page, BufferGetPageSize(buf));
+	pageop = BTPageGetOpaque(page);
+
+	pageop->btpo_prev = xlrec->left_prev;
+	pageop->btpo_next = xlrec->left_next;
+	pageop->btpo_level = 0;
+	pageop->btpo_flags = BTP_HALF_DEAD | BTP_LEAF;
+	pageop->btpo_cycleid = 0;
+
+	MemSet(&trunctuple, 0, sizeof(IndexTupleData));
+	trunctuple.t_info = sizeof(IndexTupleData);
+	BTreeTupleSetTopParent(&trunctuple, InvalidBlockNumber);
+	if (PageAddItem(page, &trunctuple, sizeof(IndexTupleData), P_HIKEY, false, false) == InvalidOffsetNumber)
+		elog(ERROR, "could not add dummy high key to half-dead page");
+
+	PageSetLSN(page, lsn);
+
+	MarkBufferDirty(buf);
+	UnlockReleaseBuffer(buf);
+}
+
 void
 btree_redo(XLogReaderState *record)
 {
@@ -1059,6 +1229,31 @@ btree_redo(XLogReaderState *record)
 	MemoryContextReset(opCtx);
 }
 
+void
+btree2_redo(XLogReaderState *record)
+{
+	uint8		info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+	MemoryContext oldCtx;
+
+	oldCtx = MemoryContextSwitchTo(opCtx);
+	switch (info)
+	{
+		case XLOG_BTREE2_MERGE:
+			btree_xlog_merge_page(record);
+			break;
+		case XLOG_BTREE2_CLEAR_MERGE_FLAG:
+			btree_xlog_clear_m_flag(record);
+			break;
+		case XLOG_BTREE2_MERGE_MARK_HALFDEAD:
+			btree_xlog_mark_ma_hd(record);
+			break;
+		default:
+			elog(PANIC, "btree2_redo: unknown op code %u", info);
+	}
+	MemoryContextSwitchTo(oldCtx);
+	MemoryContextReset(opCtx);
+}
+
 void
 btree_xlog_startup(void)
 {
@@ -1082,14 +1277,20 @@ btree_mask(char *pagedata, BlockNumber blkno)
 {
 	Page		page = (Page) pagedata;
 	BTPageOpaque maskopaq;
+	PageHeader	phdr = (PageHeader) page;
+	TransactionId saved_prune_xid;
 
-	mask_page_lsn_and_checksum(page);
+	saved_prune_xid = phdr->pd_prune_xid;
 
+	mask_page_lsn_and_checksum(page);
 	mask_page_hint_bits(page);
 	mask_unused_space(page);
 
 	maskopaq = BTPageGetOpaque(page);
 
+	if(P_ISMERGED(maskopaq))
+		phdr->pd_prune_xid = saved_prune_xid;
+
 	if (P_ISLEAF(maskopaq))
 	{
 		/*
diff --git a/src/backend/access/rmgrdesc/nbtdesc.c b/src/backend/access/rmgrdesc/nbtdesc.c
index 1d08f9957bd..1c7b1c7e575 100644
--- a/src/backend/access/rmgrdesc/nbtdesc.c
+++ b/src/backend/access/rmgrdesc/nbtdesc.c
@@ -132,6 +132,42 @@ btree_desc(StringInfo buf, XLogReaderState *record)
 								 xlrec->last_cleanup_num_delpages);
 				break;
 			}
+
+	}
+}
+
+void
+btree2_desc(StringInfo buf, XLogReaderState *record)
+{
+	char	   *rec = XLogRecGetData(record);
+	uint8		info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+	switch (info)
+	{
+		case XLOG_BTREE2_MERGE:
+		{
+			xl_btree_merge *xlrec = (xl_btree_merge *) rec;
+			appendStringInfo(buf,
+		        "merge pages: left_prev: %u, left_next: %u, "
+		        "poffset: %u, safexid: %u:%u",
+		        xlrec->left_prev, xlrec->left_next,
+		        xlrec->poffset,
+		        EpochFromFullTransactionId(xlrec->safemergexid),
+		        XidFromFullTransactionId(xlrec->safemergexid));
+		    break;
+		}
+		case XLOG_BTREE2_CLEAR_MERGE_FLAG:
+		{	appendStringInfoString(buf, "clear merge flag");
+		    break;
+		}
+		case XLOG_BTREE2_MERGE_MARK_HALFDEAD:
+		{
+			xl_btree_mark_ma_hd *xlrec = (xl_btree_mark_ma_hd *) rec;
+			appendStringInfo(buf,
+		        "mark merged-away page half-dead: left: %u, right: %u",
+		        xlrec->left_prev, xlrec->left_next);
+		    break;
+		}
 	}
 }
 
@@ -192,6 +228,25 @@ btree_identify(uint8 info)
 	return id;
 }
 
+const char *
+btree2_identify(uint8 info){
+	const char *id = NULL;
+
+	switch (info & ~XLR_INFO_MASK)
+	{
+		case XLOG_BTREE2_MERGE:
+			id = "MERGE";
+			break;
+		case XLOG_BTREE2_CLEAR_MERGE_FLAG:
+			id = "CLEAR_MERGED_FLAG";
+			break;
+		case XLOG_BTREE2_MERGE_MARK_HALFDEAD:
+			id = "MARK_MA_AS_HD";
+			break;
+	}
+	return id;
+}
+
 static void
 delvacuum_desc(StringInfo buf, char *block_data,
 			   uint16 ndeleted, uint16 nupdated)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 7b33efc6299..006c17e6a8a 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -96,7 +96,8 @@ CommitTs
 ReplicationOrigin
 Generic
 LogicalMessage
-XLOG2$/,
+XLOG2
+Btree2$/,
 	'rmgr list');
 
 
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 3097e9bb1af..a5c18319d56 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -83,6 +83,9 @@ typedef BTPageOpaqueData *BTPageOpaque;
 #define BTP_HAS_GARBAGE (1 << 6)	/* page has LP_DEAD tuples (deprecated) */
 #define BTP_INCOMPLETE_SPLIT (1 << 7)	/* right sibling's downlink is missing */
 #define BTP_HAS_FULLXID	(1 << 8)	/* contains BTDeletedPageData */
+#define BTP_MERGED		(1 << 9)	/* This node contain its lift sibling data */
+#define BTP_MERGED_AWAY	(1 << 10)	/* This node was merged into its right
+									 * sibling */
 
 /*
  * The max allowed value of a cycle ID is a bit less than 64K.  This is
@@ -227,6 +230,103 @@ typedef struct BTMetaPageData
 #define P_HAS_GARBAGE(opaque)	(((opaque)->btpo_flags & BTP_HAS_GARBAGE) != 0)
 #define P_INCOMPLETE_SPLIT(opaque)	(((opaque)->btpo_flags & BTP_INCOMPLETE_SPLIT) != 0)
 #define P_HAS_FULLXID(opaque)	(((opaque)->btpo_flags & BTP_HAS_FULLXID) != 0)
+#define P_ISMERGED(opaque)		(((opaque)->btpo_flags & BTP_MERGED) != 0)
+#define P_ISMERGEDAWAY(opaque)	(((opaque)->btpo_flags & BTP_MERGED_AWAY) != 0)
+
+/*
+ * Accessors for the MERGED_AWAY block number stored on BTP_MERGED pages.
+ *
+ * We store the block number of the corresponding BTP_MERGED_AWAY (tombstone)
+ * page in pd_prune_xid, which is a 4-byte field in PageHeaderData that is
+ * explicitly documented as "currently unused in index pages" (see bufpage.h).
+ * This is the only available 4-byte slot on a MERGED page that may be
+ * completely full of index tuples -- we cannot extend the special area on
+ * a full page, and the item content area is occupied by live tuples.
+ *
+ * Only valid when P_MERGED(opaque) is true.  All other B-tree pages keep
+ * pd_prune_xid at its default value of InvalidTransactionId (0).
+ */
+#define BTMergedPageGetMABlkno(page) \
+	((BlockNumber) ((PageHeader)(page))->pd_prune_xid)
+
+#define BTMergedPageSetMABlkno(page, blkno) \
+	(((PageHeader)(page))->pd_prune_xid = (TransactionId)(blkno))
+
+/*
+ * Clear the MA block number from a MERGED page when the BTP_MERGED flag is
+ * being cleared (e.g., during vacuum cleanup of the merge group).
+ * Restores pd_prune_xid to its standard value of InvalidTransactionId (0),
+ * since index pages normally never use that field.
+ */
+#define BTMergedPageClearMABlkno(page) \
+	(((PageHeader)(page))->pd_prune_xid = InvalidTransactionId)
+
+
+typedef struct BTMergedAwayPageData
+{
+	FullTransactionId safemergexid;
+}			BTMergedAwayPageData;
+
+static inline void
+BTPageSetMerged(Page page)
+{
+	BTPageOpaque opaque;
+
+	opaque = BTPageGetOpaque(page);
+	opaque->btpo_flags |= BTP_MERGED;
+
+}
+
+static inline void
+BTPageSetMergedAway(Page page, FullTransactionId safemergexid)
+{
+	BTPageOpaque opaque;
+	PageHeader	header;
+	BTMergedAwayPageData *contents;
+
+	opaque = BTPageGetOpaque(page);
+	header = ((PageHeader) page);
+
+	opaque->btpo_flags |= BTP_MERGED_AWAY | BTP_HAS_FULLXID;
+	header->pd_lower = MAXALIGN(SizeOfPageHeaderData) +
+		sizeof(BTMergedAwayPageData);
+	header->pd_upper = header->pd_special;
+
+	contents = (BTMergedAwayPageData *) PageGetContents(page);
+	contents->safemergexid = safemergexid;
+}
+
+static inline FullTransactionId
+BTMergedAwayGetSafeXid(Page page)
+{
+	BTPageOpaque opaque;
+	BTMergedAwayPageData *contents;
+
+	opaque = BTPageGetOpaque(page);
+	Assert(P_ISMERGEDAWAY(opaque));
+
+	if (!P_HAS_FULLXID(opaque))
+		return FirstNormalFullTransactionId;
+
+	contents = (BTMergedAwayPageData *) PageGetContents(page);
+	return contents->safemergexid;
+}
+
+/*
+ * BTPageIsMergedMember -- true when a page is a valid member of the merge
+ * group whose tombstone (MA page) is at ma_blkno.
+ *
+ * Used during VACUUM cleanup to validate each M page before clearing its
+ * flags, preventing accidental absorption of pages from an adjacent group.
+ */
+static inline bool
+BTPageIsMergedMember(BTPageOpaque opq, Page pg, BlockNumber ma_blkno)
+{
+	return P_ISMERGED(opq) &&
+		P_ISLEAF(opq) &&
+		!P_ISMERGEDAWAY(opq) &&
+		BTMergedPageGetMABlkno(pg) == ma_blkno;
+}
 
 /*
  * BTDeletedPageData is the page contents of a deleted page
@@ -1092,6 +1192,15 @@ typedef struct BTScanOpaqueData
 	/* keep these last in struct for efficiency */
 	BTScanPosData currPos;		/* current position data */
 	BTScanPosData markPos;		/* marked position, if any */
+
+	/* Merge information */
+	bool		skipMergeRecovery;
+	bool		needMergeRecovery;
+
+	ItemPointerData savedMergeTids[MaxTIDsPerBTreePage];
+	int			nSavedMergeTids;	/* number of TIDs in the array */
+	BlockNumber mergedAwayBlkno;	/* blkno of L (BTP_MERGED_AWAY page) */
+
 } BTScanOpaqueData;
 
 typedef BTScanOpaqueData *BTScanOpaque;
@@ -1219,6 +1328,7 @@ extern void _bt_finish_split(Relation rel, Relation heaprel, Buffer lbuf,
 							 BTStack stack);
 extern Buffer _bt_getstackbuf(Relation rel, Relation heaprel, BTStack stack,
 							  BlockNumber child);
+extern void _bt_freestack(BTStack stack);
 
 /*
  * prototypes for functions in nbtsplitloc.c
@@ -1262,6 +1372,8 @@ extern void _bt_pagedel(Relation rel, Buffer leafbuf, BTVacState *vstate);
 extern void _bt_pendingfsm_init(Relation rel, BTVacState *vstate,
 								bool cleanuponly);
 extern void _bt_pendingfsm_finalize(Relation rel, BTVacState *vstate);
+extern bool _bt_pages_share_parent(Relation rel, BlockNumber left_blkno,
+								   BlockNumber right_blkno, BTScanInsert scankey, BTStack *stack_out);
 
 /*
  * prototypes for functions in nbtpreprocesskeys.c
@@ -1331,4 +1443,11 @@ extern IndexBuildResult *btbuild(Relation heap, Relation index,
 								 struct IndexInfo *indexInfo);
 extern void _bt_parallel_build_main(dsm_segment *seg, shm_toc *toc);
 
+/**
+ * prototypes for functions in nbmerge.c
+ */
+
+extern int32 _bt_merge_index(Relation rel, float8 min_pct, float8 dest_pct, int32 num_pages);
+
+
 #endif							/* NBTREE_H */
diff --git a/src/include/access/nbtxlog.h b/src/include/access/nbtxlog.h
index 3a78ec27fe8..d89e2008164 100644
--- a/src/include/access/nbtxlog.h
+++ b/src/include/access/nbtxlog.h
@@ -43,6 +43,16 @@
 #define XLOG_BTREE_META_CLEANUP	0xE0	/* update cleanup-related data in the
 										 * metapage */
 
+
+
+/*
+ * We ran out of opcodes, so btree now has a second RmgrId.  These opcodes
+ * are associated with RM_BTREE2_ID.
+ */
+#define XLOG_BTREE2_MERGE					0x00
+#define XLOG_BTREE2_CLEAR_MERGE_FLAG			0x10	
+#define XLOG_BTREE2_MERGE_MARK_HALFDEAD		0x20
+
 /*
  * All that we need to regenerate the meta-data page
  */
@@ -156,9 +166,10 @@ typedef struct xl_btree_split
 	OffsetNumber firstrightoff; /* first origpage item on rightpage */
 	OffsetNumber newitemoff;	/* new item's offset */
 	uint16		postingoff;		/* offset inside orig posting tuple */
+	BlockNumber merged_ma_blkno;
 } xl_btree_split;
 
-#define SizeOfBtreeSplit	(offsetof(xl_btree_split, postingoff) + sizeof(uint16))
+#define SizeOfBtreeSplit	(offsetof(xl_btree_split, merged_ma_blkno) + sizeof(BlockNumber))
 
 /*
  * When page is deduplicated, consecutive groups of tuples with equal keys are
@@ -170,11 +181,11 @@ typedef struct xl_btree_split
 typedef struct xl_btree_dedup
 {
 	uint16		nintervals;
-
+	BlockNumber merged_ma_blkno;
 	/* DEDUPLICATION INTERVALS FOLLOW */
 } xl_btree_dedup;
 
-#define SizeOfBtreeDedup 	(offsetof(xl_btree_dedup, nintervals) + sizeof(uint16))
+#define SizeOfBtreeDedup 	(offsetof(xl_btree_dedup, merged_ma_blkno) + sizeof(BlockNumber))
 
 /*
  * This is what we need to know about page reuse within btree.  This record
@@ -350,6 +361,33 @@ typedef struct xl_btree_newroot
 #define SizeOfBtreeNewroot	(offsetof(xl_btree_newroot, level) + sizeof(uint32))
 
 
+typedef struct xl_btree_merge
+{
+	BlockNumber  left_prev;      /* L's left sibling (for btpo_prev of MA page) */
+	BlockNumber  left_next;      /* L's right sibling (for btpo_prev of MA page) */ 
+	OffsetNumber poffset;		/* offset of L's downlink in parent */
+	FullTransactionId safemergexid;
+} xl_btree_merge;
+#define SizeOfBtreeMerge	(offsetof(xl_btree_merge, safemergexid) + sizeof(uint64))
+
+typedef struct xl_btree_clear_m
+{
+	bool		isCatalogRel;	/* to handle recovery conflict during logical
+								* decoding on standby */
+	FullTransactionId safemergexid;
+} xl_btree_clear_m;
+#define SizeOfBtreeClearM 	(offsetof(xl_btree_clear_m, safemergexid) + sizeof(uint64))
+
+typedef struct xl_btree_mark_ma_hd
+{
+	BlockNumber  left_prev;      /* L's left sibling (for btpo_prev of MA page) */
+	BlockNumber  left_next;      /* L's right sibling (for btpo_prev of MA page) */ 
+	bool		isCatalogRel;	/* to handle recovery conflict during logical
+								 * decoding on standby */
+	FullTransactionId safemergexid;
+} xl_btree_mark_ma_hd;
+#define SizeOfBtreeMarkMaHd 	(offsetof(xl_btree_mark_ma_hd, safemergexid) + sizeof(uint64))
+
 /*
  * prototypes for functions in nbtxlog.c
  */
@@ -357,11 +395,14 @@ extern void btree_redo(XLogReaderState *record);
 extern void btree_xlog_startup(void);
 extern void btree_xlog_cleanup(void);
 extern void btree_mask(char *pagedata, BlockNumber blkno);
+extern void btree2_redo(XLogReaderState *record);
 
 /*
  * prototypes for functions in nbtdesc.c
  */
 extern void btree_desc(StringInfo buf, XLogReaderState *record);
 extern const char *btree_identify(uint8 info);
+extern void btree2_desc(StringInfo buf, XLogReaderState *record);
+extern const char *btree2_identify(uint8 info);
 
 #endif							/* NBTXLOG_H */
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index ae32ef16d67..9defafafdf7 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -48,3 +48,4 @@ PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc,
 PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
 PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
 PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
+PG_RMGR(RM_BTREE2_ID, "Btree2", btree2_redo, btree2_desc, btree2_identify, NULL, NULL, btree_mask, NULL)
-- 
2.43.0

