agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
[PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
10+ messages / 4 participants
[nested] [flat]

* [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
@ 2024-03-19 22:50 Melanie Plageman <melanieplageman@gmail.com>
  0 siblings, 0 replies; 10+ messages in thread

From: Melanie Plageman @ 2024-03-19 22:50 UTC (permalink / raw)

Remove the XLOG_HEAP2_VACUUM record and update vacuum's second pass to
emit a XLOG_HEAP2_PRUNE record. This temporarily wastes some space but a
future commit will streamline xl_heap_prune and ensure that no unused
members are included in the WAL record.
---
 src/backend/access/heap/heapam.c         | 94 ++++--------------------
 src/backend/access/heap/pruneheap.c      | 67 ++++++++++-------
 src/backend/access/heap/vacuumlazy.c     | 12 ++-
 src/backend/access/rmgrdesc/heapdesc.c   | 20 -----
 src/backend/replication/logical/decode.c |  1 -
 src/include/access/heapam.h              |  2 +-
 src/include/access/heapam_xlog.h         | 33 +++++----
 7 files changed, 85 insertions(+), 144 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 532868039d5..16bab55ba02 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -8717,23 +8717,34 @@ heap_xlog_prune(XLogReaderState *record)
 	BlockNumber blkno;
 	XLogRedoAction action;
 	bool		get_cleanup_lock;
+	bool		lp_truncate_only;
 
 	XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
 
+	lp_truncate_only = xlrec->flags & XLHP_LP_TRUNCATE_ONLY;
+
 	/*
 	 * If there are dead, redirected, or unused items set unused by
 	 * heap_page_prune_and_freeze(), heap_page_prune_execute() will call
 	 * PageRepairFragementation() which expects a full cleanup lock.
 	 */
 	get_cleanup_lock = xlrec->nredirected > 0 ||
-		xlrec->ndead > 0 || xlrec->nunused > 0;
+		xlrec->ndead > 0 ||
+		(xlrec->nunused > 0 && !lp_truncate_only);
+
+	if (lp_truncate_only)
+	{
+		Assert(xlrec->nredirected == 0);
+		Assert(xlrec->ndead == 0);
+		Assert(xlrec->nunused > 0);
+	}
 
 	/*
 	 * We are either about to remove tuples or freeze them. In Hot Standby
 	 * mode, ensure that there's no queries running for which any removed
 	 * tuples are still visible or which consider the frozen xids as running.
 	 */
-	if (InHotStandby)
+	if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON && InHotStandby)
 		ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon,
 											xlrec->isCatalogRel,
 											rlocator);
@@ -8772,7 +8783,7 @@ heap_xlog_prune(XLogReaderState *record)
 
 		/* Update all line pointers per the record, and repair fragmentation */
 		if (nredirected > 0 || ndead > 0 || nunused > 0)
-			heap_page_prune_execute(buffer,
+			heap_page_prune_execute(buffer, lp_truncate_only,
 									redirected, nredirected,
 									nowdead, ndead,
 									nowunused, nunused);
@@ -8819,7 +8830,7 @@ heap_xlog_prune(XLogReaderState *record)
 		UnlockReleaseBuffer(buffer);
 
 		/*
-		 * After pruning records from a page, it's useful to update the FSM
+		 * After modifying records on a page, it's useful to update the FSM
 		 * about it, as it may cause the page become target for insertions
 		 * later even if vacuum decides not to visit it (which is possible if
 		 * gets marked all-visible.)
@@ -8831,78 +8842,6 @@ heap_xlog_prune(XLogReaderState *record)
 	}
 }
 
-/*
- * Handles XLOG_HEAP2_VACUUM record type.
- *
- * Acquires an ordinary exclusive lock only.
- */
-static void
-heap_xlog_vacuum(XLogReaderState *record)
-{
-	XLogRecPtr	lsn = record->EndRecPtr;
-	xl_heap_vacuum *xlrec = (xl_heap_vacuum *) XLogRecGetData(record);
-	Buffer		buffer;
-	BlockNumber blkno;
-	XLogRedoAction action;
-
-	/*
-	 * If we have a full-page image, restore it	(without using a cleanup lock)
-	 * and we're done.
-	 */
-	action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, false,
-										   &buffer);
-	if (action == BLK_NEEDS_REDO)
-	{
-		Page		page = (Page) BufferGetPage(buffer);
-		OffsetNumber *nowunused;
-		Size		datalen;
-		OffsetNumber *offnum;
-
-		nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, &datalen);
-
-		/* Shouldn't be a record unless there's something to do */
-		Assert(xlrec->nunused > 0);
-
-		/* Update all now-unused line pointers */
-		offnum = nowunused;
-		for (int i = 0; i < xlrec->nunused; i++)
-		{
-			OffsetNumber off = *offnum++;
-			ItemId		lp = PageGetItemId(page, off);
-
-			Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
-			ItemIdSetUnused(lp);
-		}
-
-		/* Attempt to truncate line pointer array now */
-		PageTruncateLinePointerArray(page);
-
-		PageSetLSN(page, lsn);
-		MarkBufferDirty(buffer);
-	}
-
-	if (BufferIsValid(buffer))
-	{
-		Size		freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
-		RelFileLocator rlocator;
-
-		XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
-
-		UnlockReleaseBuffer(buffer);
-
-		/*
-		 * After vacuuming LP_DEAD items from a page, it's useful to update
-		 * the FSM about it, as it may cause the page become target for
-		 * insertions later even if vacuum decides not to visit it (which is
-		 * possible if gets marked all-visible.)
-		 *
-		 * Do this regardless of a full-page image being applied, since the
-		 * FSM data is not in the page anyway.
-		 */
-		XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
-	}
-}
-
 /*
  * Replay XLOG_HEAP2_VISIBLE record.
  *
@@ -9943,9 +9882,6 @@ heap2_redo(XLogReaderState *record)
 		case XLOG_HEAP2_PRUNE:
 			heap_xlog_prune(record);
 			break;
-		case XLOG_HEAP2_VACUUM:
-			heap_xlog_vacuum(record);
-			break;
 		case XLOG_HEAP2_VISIBLE:
 			heap_xlog_visible(record);
 			break;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 19b50931b90..135fe2dba3e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -601,7 +601,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 		 */
 		if (do_prune)
 		{
-			heap_page_prune_execute(buffer,
+			heap_page_prune_execute(buffer, false,
 									prstate.redirected, prstate.nredirected,
 									prstate.nowdead, prstate.ndead,
 									prstate.nowunused, prstate.nunused);
@@ -668,12 +668,16 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 	OffsetNumber offsets[MaxHeapTuplesPerPage];
 	bool		do_freeze = presult->nfrozen > 0;
 
+	xlrec.flags = 0;
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(relation);
 	xlrec.nredirected = prstate->nredirected;
 	xlrec.ndead = prstate->ndead;
 	xlrec.nunused = prstate->nunused;
 	xlrec.nplans = 0;
 
+	xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
+
 	/*
 	 * The snapshotConflictHorizon for the whole record should be the most
 	 * conservative of all the horizons calculated for any of the possible
@@ -1149,7 +1153,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum)
  * cleanup lock on the buffer.
  */
 void
-heap_page_prune_execute(Buffer buffer,
+heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 						OffsetNumber *redirected, int nredirected,
 						OffsetNumber *nowdead, int ndead,
 						OffsetNumber *nowunused, int nunused)
@@ -1171,6 +1175,7 @@ heap_page_prune_execute(Buffer buffer,
 		ItemId		tolp PG_USED_FOR_ASSERTS_ONLY;
 
 #ifdef USE_ASSERT_CHECKING
+		Assert(!lp_truncate_only);
 
 		/*
 		 * Any existing item that we set as an LP_REDIRECT (any 'from' item)
@@ -1226,6 +1231,7 @@ heap_page_prune_execute(Buffer buffer,
 		ItemId		lp = PageGetItemId(page, off);
 
 #ifdef USE_ASSERT_CHECKING
+		Assert(!lp_truncate_only);
 
 		/*
 		 * An LP_DEAD line pointer must be left behind when the original item
@@ -1259,23 +1265,29 @@ heap_page_prune_execute(Buffer buffer,
 
 #ifdef USE_ASSERT_CHECKING
 
-		/*
-		 * When heap_page_prune_and_freeze() was called, mark_unused_now may
-		 * have been passed as true, which allows would-be LP_DEAD items to be
-		 * made LP_UNUSED instead. This is only possible if the relation has
-		 * no indexes. If there are any dead items, then mark_unused_now was
-		 * not true and every item being marked LP_UNUSED must refer to a
-		 * heap-only tuple.
-		 */
-		if (ndead > 0)
+		if (lp_truncate_only)
 		{
-			Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
-			htup = (HeapTupleHeader) PageGetItem(page, lp);
-			Assert(HeapTupleHeaderIsHeapOnly(htup));
+			/* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
+			Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
 		}
 		else
 		{
-			Assert(ItemIdIsUsed(lp));
+			/*
+			 * When heap_page_prune_and_freeze() was called, mark_unused_now
+			 * may have been passed as true, which allows would-be LP_DEAD
+			 * items to be made LP_UNUSED instead. This is only possible if
+			 * the relation has no indexes. If there are any dead items, then
+			 * mark_unused_now was not true and every item being marked
+			 * LP_UNUSED must refer to a heap-only tuple.
+			 */
+			if (ndead > 0)
+			{
+				Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
+				htup = (HeapTupleHeader) PageGetItem(page, lp);
+				Assert(HeapTupleHeaderIsHeapOnly(htup));
+			}
+			else
+				Assert(ItemIdIsUsed(lp));
 		}
 
 #endif
@@ -1283,17 +1295,22 @@ heap_page_prune_execute(Buffer buffer,
 		ItemIdSetUnused(lp);
 	}
 
-	/*
-	 * Finally, repair any fragmentation, and update the page's hint bit about
-	 * whether it has free pointers.
-	 */
-	PageRepairFragmentation(page);
+	if (lp_truncate_only)
+		PageTruncateLinePointerArray(page);
+	else
+	{
+		/*
+		 * Finally, repair any fragmentation, and update the page's hint bit
+		 * about whether it has free pointers.
+		 */
+		PageRepairFragmentation(page);
 
-	/*
-	 * Now that the page has been modified, assert that redirect items still
-	 * point to valid targets.
-	 */
-	page_verify_redirects(page);
+		/*
+		 * Now that the page has been modified, assert that redirect items
+		 * still point to valid targets.
+		 */
+		page_verify_redirects(page);
+	}
 }
 
 
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c4553a4159c..9dfb56475cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -2394,18 +2394,24 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
 	/* XLOG stuff */
 	if (RelationNeedsWAL(vacrel->rel))
 	{
-		xl_heap_vacuum xlrec;
+		xl_heap_prune xlrec;
 		XLogRecPtr	recptr;
 
+		xlrec.flags = XLHP_LP_TRUNCATE_ONLY;
+		xlrec.snapshotConflictHorizon = InvalidTransactionId;
+		xlrec.nplans = 0;
+		xlrec.nredirected = 0;
+		xlrec.ndead = 0;
 		xlrec.nunused = nunused;
+		xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(vacrel->rel);
 
 		XLogBeginInsert();
-		XLogRegisterData((char *) &xlrec, SizeOfHeapVacuum);
+		XLogRegisterData((char *) &xlrec, SizeOfHeapPrune);
 
 		XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
 		XLogRegisterBufData(0, (char *) unused, nunused * sizeof(OffsetNumber));
 
-		recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);
+		recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_PRUNE);
 
 		PageSetLSN(page, recptr);
 	}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index 9f0a0341d40..ea03f902fc4 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -242,23 +242,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
 			}
 		}
 	}
-	else if (info == XLOG_HEAP2_VACUUM)
-	{
-		xl_heap_vacuum *xlrec = (xl_heap_vacuum *) rec;
-
-		appendStringInfo(buf, "nunused: %u", xlrec->nunused);
-
-		if (XLogRecHasBlockData(record, 0))
-		{
-			OffsetNumber *nowunused;
-
-			nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, NULL);
-
-			appendStringInfoString(buf, ", unused:");
-			array_desc(buf, nowunused, sizeof(OffsetNumber), xlrec->nunused,
-					   &offset_elem_desc, NULL);
-		}
-	}
 	else if (info == XLOG_HEAP2_VISIBLE)
 	{
 		xl_heap_visible *xlrec = (xl_heap_visible *) rec;
@@ -360,9 +343,6 @@ heap2_identify(uint8 info)
 		case XLOG_HEAP2_PRUNE:
 			id = "PRUNE";
 			break;
-		case XLOG_HEAP2_VACUUM:
-			id = "VACUUM";
-			break;
 		case XLOG_HEAP2_VISIBLE:
 			id = "VISIBLE";
 			break;
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index f77051572fd..38d1bdd825e 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -446,7 +446,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 			 * interested in.
 			 */
 		case XLOG_HEAP2_PRUNE:
-		case XLOG_HEAP2_VACUUM:
 		case XLOG_HEAP2_VISIBLE:
 		case XLOG_HEAP2_LOCK_UPDATED:
 			break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 321a46185e1..d5cb8f99cac 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -349,7 +349,7 @@ extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 									   HeapPageFreeze *pagefrz,
 									   PruneFreezeResult *presult,
 									   OffsetNumber *off_loc);
-extern void heap_page_prune_execute(Buffer buffer,
+extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 									OffsetNumber *redirected, int nredirected,
 									OffsetNumber *nowdead, int ndead,
 									OffsetNumber *nowunused, int nunused);
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index fe4a8ff0620..2393540cf68 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -52,11 +52,10 @@
  */
 #define XLOG_HEAP2_REWRITE		0x00
 #define XLOG_HEAP2_PRUNE		0x10
-#define XLOG_HEAP2_VACUUM		0x20
-#define XLOG_HEAP2_VISIBLE		0x30
-#define XLOG_HEAP2_MULTI_INSERT 0x40
-#define XLOG_HEAP2_LOCK_UPDATED 0x50
-#define XLOG_HEAP2_NEW_CID		0x60
+#define XLOG_HEAP2_VISIBLE		0x20
+#define XLOG_HEAP2_MULTI_INSERT 0x30
+#define XLOG_HEAP2_LOCK_UPDATED 0x40
+#define XLOG_HEAP2_NEW_CID		0x50
 
 /*
  * xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
@@ -266,6 +265,7 @@ typedef struct xl_heap_freeze_plan
  */
 typedef struct xl_heap_prune
 {
+	uint8		flags;
 	TransactionId snapshotConflictHorizon;
 	uint16		nplans;
 	uint16		nredirected;
@@ -288,19 +288,22 @@ typedef struct xl_heap_prune
 
 #define SizeOfHeapPrune (offsetof(xl_heap_prune, isCatalogRel) + sizeof(bool))
 
+/* Flags for xl_heap_prune */
+
 /*
- * The vacuum page record is similar to the prune record, but can only mark
- * already LP_DEAD items LP_UNUSED (during VACUUM's second heap pass)
- *
- * Acquires an ordinary exclusive lock only.
+ * During vacuum's second pass which sets LP_DEAD items LP_UNUSED, we will only
+ * truncate the line pointer array, not call PageRepairFragmentation. We need
+ * this flag to differentiate what kind of lock (exclusive or cleanup) to take
+ * on the buffer and whether to call PageTruncateLinePointerArray() or
+ * PageRepairFragementation().
  */
-typedef struct xl_heap_vacuum
-{
-	uint16		nunused;
-	/* OFFSET NUMBERS are in the block reference 0 */
-} xl_heap_vacuum;
+#define		XLHP_LP_TRUNCATE_ONLY       (1 << 1)
 
-#define SizeOfHeapVacuum (offsetof(xl_heap_vacuum, nunused) + sizeof(uint16))
+/*
+ * Vacuum's first pass and on-access pruning may need to include a snapshot
+ * conflict horizon.
+ */
+#define		XLHP_HAS_CONFLICT_HORIZON   (1 << 2)
 
 /* flags for infobits_set */
 #define XLHL_XMAX_IS_MULTI		0x01
-- 
2.40.1


--tez7m2a73jtztiij
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0015-Set-hastup-in-heap_page_prune.patch"



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
@ 2024-03-19 22:50 Melanie Plageman <melanieplageman@gmail.com>
  0 siblings, 0 replies; 10+ messages in thread

From: Melanie Plageman @ 2024-03-19 22:50 UTC (permalink / raw)

Remove the XLOG_HEAP2_VACUUM record and update vacuum's second pass to
emit a XLOG_HEAP2_PRUNE record. This temporarily wastes some space but a
future commit will streamline xl_heap_prune and ensure that no unused
members are included in the WAL record.
---
 src/backend/access/heap/heapam.c         | 94 ++++--------------------
 src/backend/access/heap/pruneheap.c      | 67 ++++++++++-------
 src/backend/access/heap/vacuumlazy.c     | 12 ++-
 src/backend/access/rmgrdesc/heapdesc.c   | 20 -----
 src/backend/replication/logical/decode.c |  1 -
 src/include/access/heapam.h              |  2 +-
 src/include/access/heapam_xlog.h         | 33 +++++----
 7 files changed, 85 insertions(+), 144 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 532868039d5..16bab55ba02 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -8717,23 +8717,34 @@ heap_xlog_prune(XLogReaderState *record)
 	BlockNumber blkno;
 	XLogRedoAction action;
 	bool		get_cleanup_lock;
+	bool		lp_truncate_only;
 
 	XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
 
+	lp_truncate_only = xlrec->flags & XLHP_LP_TRUNCATE_ONLY;
+
 	/*
 	 * If there are dead, redirected, or unused items set unused by
 	 * heap_page_prune_and_freeze(), heap_page_prune_execute() will call
 	 * PageRepairFragementation() which expects a full cleanup lock.
 	 */
 	get_cleanup_lock = xlrec->nredirected > 0 ||
-		xlrec->ndead > 0 || xlrec->nunused > 0;
+		xlrec->ndead > 0 ||
+		(xlrec->nunused > 0 && !lp_truncate_only);
+
+	if (lp_truncate_only)
+	{
+		Assert(xlrec->nredirected == 0);
+		Assert(xlrec->ndead == 0);
+		Assert(xlrec->nunused > 0);
+	}
 
 	/*
 	 * We are either about to remove tuples or freeze them. In Hot Standby
 	 * mode, ensure that there's no queries running for which any removed
 	 * tuples are still visible or which consider the frozen xids as running.
 	 */
-	if (InHotStandby)
+	if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON && InHotStandby)
 		ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon,
 											xlrec->isCatalogRel,
 											rlocator);
@@ -8772,7 +8783,7 @@ heap_xlog_prune(XLogReaderState *record)
 
 		/* Update all line pointers per the record, and repair fragmentation */
 		if (nredirected > 0 || ndead > 0 || nunused > 0)
-			heap_page_prune_execute(buffer,
+			heap_page_prune_execute(buffer, lp_truncate_only,
 									redirected, nredirected,
 									nowdead, ndead,
 									nowunused, nunused);
@@ -8819,7 +8830,7 @@ heap_xlog_prune(XLogReaderState *record)
 		UnlockReleaseBuffer(buffer);
 
 		/*
-		 * After pruning records from a page, it's useful to update the FSM
+		 * After modifying records on a page, it's useful to update the FSM
 		 * about it, as it may cause the page become target for insertions
 		 * later even if vacuum decides not to visit it (which is possible if
 		 * gets marked all-visible.)
@@ -8831,78 +8842,6 @@ heap_xlog_prune(XLogReaderState *record)
 	}
 }
 
-/*
- * Handles XLOG_HEAP2_VACUUM record type.
- *
- * Acquires an ordinary exclusive lock only.
- */
-static void
-heap_xlog_vacuum(XLogReaderState *record)
-{
-	XLogRecPtr	lsn = record->EndRecPtr;
-	xl_heap_vacuum *xlrec = (xl_heap_vacuum *) XLogRecGetData(record);
-	Buffer		buffer;
-	BlockNumber blkno;
-	XLogRedoAction action;
-
-	/*
-	 * If we have a full-page image, restore it	(without using a cleanup lock)
-	 * and we're done.
-	 */
-	action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, false,
-										   &buffer);
-	if (action == BLK_NEEDS_REDO)
-	{
-		Page		page = (Page) BufferGetPage(buffer);
-		OffsetNumber *nowunused;
-		Size		datalen;
-		OffsetNumber *offnum;
-
-		nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, &datalen);
-
-		/* Shouldn't be a record unless there's something to do */
-		Assert(xlrec->nunused > 0);
-
-		/* Update all now-unused line pointers */
-		offnum = nowunused;
-		for (int i = 0; i < xlrec->nunused; i++)
-		{
-			OffsetNumber off = *offnum++;
-			ItemId		lp = PageGetItemId(page, off);
-
-			Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
-			ItemIdSetUnused(lp);
-		}
-
-		/* Attempt to truncate line pointer array now */
-		PageTruncateLinePointerArray(page);
-
-		PageSetLSN(page, lsn);
-		MarkBufferDirty(buffer);
-	}
-
-	if (BufferIsValid(buffer))
-	{
-		Size		freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
-		RelFileLocator rlocator;
-
-		XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
-
-		UnlockReleaseBuffer(buffer);
-
-		/*
-		 * After vacuuming LP_DEAD items from a page, it's useful to update
-		 * the FSM about it, as it may cause the page become target for
-		 * insertions later even if vacuum decides not to visit it (which is
-		 * possible if gets marked all-visible.)
-		 *
-		 * Do this regardless of a full-page image being applied, since the
-		 * FSM data is not in the page anyway.
-		 */
-		XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
-	}
-}
-
 /*
  * Replay XLOG_HEAP2_VISIBLE record.
  *
@@ -9943,9 +9882,6 @@ heap2_redo(XLogReaderState *record)
 		case XLOG_HEAP2_PRUNE:
 			heap_xlog_prune(record);
 			break;
-		case XLOG_HEAP2_VACUUM:
-			heap_xlog_vacuum(record);
-			break;
 		case XLOG_HEAP2_VISIBLE:
 			heap_xlog_visible(record);
 			break;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 19b50931b90..135fe2dba3e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -601,7 +601,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 		 */
 		if (do_prune)
 		{
-			heap_page_prune_execute(buffer,
+			heap_page_prune_execute(buffer, false,
 									prstate.redirected, prstate.nredirected,
 									prstate.nowdead, prstate.ndead,
 									prstate.nowunused, prstate.nunused);
@@ -668,12 +668,16 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 	OffsetNumber offsets[MaxHeapTuplesPerPage];
 	bool		do_freeze = presult->nfrozen > 0;
 
+	xlrec.flags = 0;
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(relation);
 	xlrec.nredirected = prstate->nredirected;
 	xlrec.ndead = prstate->ndead;
 	xlrec.nunused = prstate->nunused;
 	xlrec.nplans = 0;
 
+	xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
+
 	/*
 	 * The snapshotConflictHorizon for the whole record should be the most
 	 * conservative of all the horizons calculated for any of the possible
@@ -1149,7 +1153,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum)
  * cleanup lock on the buffer.
  */
 void
-heap_page_prune_execute(Buffer buffer,
+heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 						OffsetNumber *redirected, int nredirected,
 						OffsetNumber *nowdead, int ndead,
 						OffsetNumber *nowunused, int nunused)
@@ -1171,6 +1175,7 @@ heap_page_prune_execute(Buffer buffer,
 		ItemId		tolp PG_USED_FOR_ASSERTS_ONLY;
 
 #ifdef USE_ASSERT_CHECKING
+		Assert(!lp_truncate_only);
 
 		/*
 		 * Any existing item that we set as an LP_REDIRECT (any 'from' item)
@@ -1226,6 +1231,7 @@ heap_page_prune_execute(Buffer buffer,
 		ItemId		lp = PageGetItemId(page, off);
 
 #ifdef USE_ASSERT_CHECKING
+		Assert(!lp_truncate_only);
 
 		/*
 		 * An LP_DEAD line pointer must be left behind when the original item
@@ -1259,23 +1265,29 @@ heap_page_prune_execute(Buffer buffer,
 
 #ifdef USE_ASSERT_CHECKING
 
-		/*
-		 * When heap_page_prune_and_freeze() was called, mark_unused_now may
-		 * have been passed as true, which allows would-be LP_DEAD items to be
-		 * made LP_UNUSED instead. This is only possible if the relation has
-		 * no indexes. If there are any dead items, then mark_unused_now was
-		 * not true and every item being marked LP_UNUSED must refer to a
-		 * heap-only tuple.
-		 */
-		if (ndead > 0)
+		if (lp_truncate_only)
 		{
-			Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
-			htup = (HeapTupleHeader) PageGetItem(page, lp);
-			Assert(HeapTupleHeaderIsHeapOnly(htup));
+			/* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
+			Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
 		}
 		else
 		{
-			Assert(ItemIdIsUsed(lp));
+			/*
+			 * When heap_page_prune_and_freeze() was called, mark_unused_now
+			 * may have been passed as true, which allows would-be LP_DEAD
+			 * items to be made LP_UNUSED instead. This is only possible if
+			 * the relation has no indexes. If there are any dead items, then
+			 * mark_unused_now was not true and every item being marked
+			 * LP_UNUSED must refer to a heap-only tuple.
+			 */
+			if (ndead > 0)
+			{
+				Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
+				htup = (HeapTupleHeader) PageGetItem(page, lp);
+				Assert(HeapTupleHeaderIsHeapOnly(htup));
+			}
+			else
+				Assert(ItemIdIsUsed(lp));
 		}
 
 #endif
@@ -1283,17 +1295,22 @@ heap_page_prune_execute(Buffer buffer,
 		ItemIdSetUnused(lp);
 	}
 
-	/*
-	 * Finally, repair any fragmentation, and update the page's hint bit about
-	 * whether it has free pointers.
-	 */
-	PageRepairFragmentation(page);
+	if (lp_truncate_only)
+		PageTruncateLinePointerArray(page);
+	else
+	{
+		/*
+		 * Finally, repair any fragmentation, and update the page's hint bit
+		 * about whether it has free pointers.
+		 */
+		PageRepairFragmentation(page);
 
-	/*
-	 * Now that the page has been modified, assert that redirect items still
-	 * point to valid targets.
-	 */
-	page_verify_redirects(page);
+		/*
+		 * Now that the page has been modified, assert that redirect items
+		 * still point to valid targets.
+		 */
+		page_verify_redirects(page);
+	}
 }
 
 
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c4553a4159c..9dfb56475cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -2394,18 +2394,24 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
 	/* XLOG stuff */
 	if (RelationNeedsWAL(vacrel->rel))
 	{
-		xl_heap_vacuum xlrec;
+		xl_heap_prune xlrec;
 		XLogRecPtr	recptr;
 
+		xlrec.flags = XLHP_LP_TRUNCATE_ONLY;
+		xlrec.snapshotConflictHorizon = InvalidTransactionId;
+		xlrec.nplans = 0;
+		xlrec.nredirected = 0;
+		xlrec.ndead = 0;
 		xlrec.nunused = nunused;
+		xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(vacrel->rel);
 
 		XLogBeginInsert();
-		XLogRegisterData((char *) &xlrec, SizeOfHeapVacuum);
+		XLogRegisterData((char *) &xlrec, SizeOfHeapPrune);
 
 		XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
 		XLogRegisterBufData(0, (char *) unused, nunused * sizeof(OffsetNumber));
 
-		recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);
+		recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_PRUNE);
 
 		PageSetLSN(page, recptr);
 	}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index 9f0a0341d40..ea03f902fc4 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -242,23 +242,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
 			}
 		}
 	}
-	else if (info == XLOG_HEAP2_VACUUM)
-	{
-		xl_heap_vacuum *xlrec = (xl_heap_vacuum *) rec;
-
-		appendStringInfo(buf, "nunused: %u", xlrec->nunused);
-
-		if (XLogRecHasBlockData(record, 0))
-		{
-			OffsetNumber *nowunused;
-
-			nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, NULL);
-
-			appendStringInfoString(buf, ", unused:");
-			array_desc(buf, nowunused, sizeof(OffsetNumber), xlrec->nunused,
-					   &offset_elem_desc, NULL);
-		}
-	}
 	else if (info == XLOG_HEAP2_VISIBLE)
 	{
 		xl_heap_visible *xlrec = (xl_heap_visible *) rec;
@@ -360,9 +343,6 @@ heap2_identify(uint8 info)
 		case XLOG_HEAP2_PRUNE:
 			id = "PRUNE";
 			break;
-		case XLOG_HEAP2_VACUUM:
-			id = "VACUUM";
-			break;
 		case XLOG_HEAP2_VISIBLE:
 			id = "VISIBLE";
 			break;
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index f77051572fd..38d1bdd825e 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -446,7 +446,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 			 * interested in.
 			 */
 		case XLOG_HEAP2_PRUNE:
-		case XLOG_HEAP2_VACUUM:
 		case XLOG_HEAP2_VISIBLE:
 		case XLOG_HEAP2_LOCK_UPDATED:
 			break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 321a46185e1..d5cb8f99cac 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -349,7 +349,7 @@ extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 									   HeapPageFreeze *pagefrz,
 									   PruneFreezeResult *presult,
 									   OffsetNumber *off_loc);
-extern void heap_page_prune_execute(Buffer buffer,
+extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 									OffsetNumber *redirected, int nredirected,
 									OffsetNumber *nowdead, int ndead,
 									OffsetNumber *nowunused, int nunused);
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index fe4a8ff0620..2393540cf68 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -52,11 +52,10 @@
  */
 #define XLOG_HEAP2_REWRITE		0x00
 #define XLOG_HEAP2_PRUNE		0x10
-#define XLOG_HEAP2_VACUUM		0x20
-#define XLOG_HEAP2_VISIBLE		0x30
-#define XLOG_HEAP2_MULTI_INSERT 0x40
-#define XLOG_HEAP2_LOCK_UPDATED 0x50
-#define XLOG_HEAP2_NEW_CID		0x60
+#define XLOG_HEAP2_VISIBLE		0x20
+#define XLOG_HEAP2_MULTI_INSERT 0x30
+#define XLOG_HEAP2_LOCK_UPDATED 0x40
+#define XLOG_HEAP2_NEW_CID		0x50
 
 /*
  * xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
@@ -266,6 +265,7 @@ typedef struct xl_heap_freeze_plan
  */
 typedef struct xl_heap_prune
 {
+	uint8		flags;
 	TransactionId snapshotConflictHorizon;
 	uint16		nplans;
 	uint16		nredirected;
@@ -288,19 +288,22 @@ typedef struct xl_heap_prune
 
 #define SizeOfHeapPrune (offsetof(xl_heap_prune, isCatalogRel) + sizeof(bool))
 
+/* Flags for xl_heap_prune */
+
 /*
- * The vacuum page record is similar to the prune record, but can only mark
- * already LP_DEAD items LP_UNUSED (during VACUUM's second heap pass)
- *
- * Acquires an ordinary exclusive lock only.
+ * During vacuum's second pass which sets LP_DEAD items LP_UNUSED, we will only
+ * truncate the line pointer array, not call PageRepairFragmentation. We need
+ * this flag to differentiate what kind of lock (exclusive or cleanup) to take
+ * on the buffer and whether to call PageTruncateLinePointerArray() or
+ * PageRepairFragementation().
  */
-typedef struct xl_heap_vacuum
-{
-	uint16		nunused;
-	/* OFFSET NUMBERS are in the block reference 0 */
-} xl_heap_vacuum;
+#define		XLHP_LP_TRUNCATE_ONLY       (1 << 1)
 
-#define SizeOfHeapVacuum (offsetof(xl_heap_vacuum, nunused) + sizeof(uint16))
+/*
+ * Vacuum's first pass and on-access pruning may need to include a snapshot
+ * conflict horizon.
+ */
+#define		XLHP_HAS_CONFLICT_HORIZON   (1 << 2)
 
 /* flags for infobits_set */
 #define XLHL_XMAX_IS_MULTI		0x01
-- 
2.40.1


--tez7m2a73jtztiij
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0015-Set-hastup-in-heap_page_prune.patch"



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
@ 2024-03-19 22:50 Melanie Plageman <melanieplageman@gmail.com>
  0 siblings, 0 replies; 10+ messages in thread

From: Melanie Plageman @ 2024-03-19 22:50 UTC (permalink / raw)

Remove the XLOG_HEAP2_VACUUM record and update vacuum's second pass to
emit a XLOG_HEAP2_PRUNE record. This temporarily wastes some space but a
future commit will streamline xl_heap_prune and ensure that no unused
members are included in the WAL record.
---
 src/backend/access/heap/heapam.c         | 94 ++++--------------------
 src/backend/access/heap/pruneheap.c      | 67 ++++++++++-------
 src/backend/access/heap/vacuumlazy.c     | 12 ++-
 src/backend/access/rmgrdesc/heapdesc.c   | 20 -----
 src/backend/replication/logical/decode.c |  1 -
 src/include/access/heapam.h              |  2 +-
 src/include/access/heapam_xlog.h         | 33 +++++----
 7 files changed, 85 insertions(+), 144 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 532868039d5..16bab55ba02 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -8717,23 +8717,34 @@ heap_xlog_prune(XLogReaderState *record)
 	BlockNumber blkno;
 	XLogRedoAction action;
 	bool		get_cleanup_lock;
+	bool		lp_truncate_only;
 
 	XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
 
+	lp_truncate_only = xlrec->flags & XLHP_LP_TRUNCATE_ONLY;
+
 	/*
 	 * If there are dead, redirected, or unused items set unused by
 	 * heap_page_prune_and_freeze(), heap_page_prune_execute() will call
 	 * PageRepairFragementation() which expects a full cleanup lock.
 	 */
 	get_cleanup_lock = xlrec->nredirected > 0 ||
-		xlrec->ndead > 0 || xlrec->nunused > 0;
+		xlrec->ndead > 0 ||
+		(xlrec->nunused > 0 && !lp_truncate_only);
+
+	if (lp_truncate_only)
+	{
+		Assert(xlrec->nredirected == 0);
+		Assert(xlrec->ndead == 0);
+		Assert(xlrec->nunused > 0);
+	}
 
 	/*
 	 * We are either about to remove tuples or freeze them. In Hot Standby
 	 * mode, ensure that there's no queries running for which any removed
 	 * tuples are still visible or which consider the frozen xids as running.
 	 */
-	if (InHotStandby)
+	if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON && InHotStandby)
 		ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon,
 											xlrec->isCatalogRel,
 											rlocator);
@@ -8772,7 +8783,7 @@ heap_xlog_prune(XLogReaderState *record)
 
 		/* Update all line pointers per the record, and repair fragmentation */
 		if (nredirected > 0 || ndead > 0 || nunused > 0)
-			heap_page_prune_execute(buffer,
+			heap_page_prune_execute(buffer, lp_truncate_only,
 									redirected, nredirected,
 									nowdead, ndead,
 									nowunused, nunused);
@@ -8819,7 +8830,7 @@ heap_xlog_prune(XLogReaderState *record)
 		UnlockReleaseBuffer(buffer);
 
 		/*
-		 * After pruning records from a page, it's useful to update the FSM
+		 * After modifying records on a page, it's useful to update the FSM
 		 * about it, as it may cause the page become target for insertions
 		 * later even if vacuum decides not to visit it (which is possible if
 		 * gets marked all-visible.)
@@ -8831,78 +8842,6 @@ heap_xlog_prune(XLogReaderState *record)
 	}
 }
 
-/*
- * Handles XLOG_HEAP2_VACUUM record type.
- *
- * Acquires an ordinary exclusive lock only.
- */
-static void
-heap_xlog_vacuum(XLogReaderState *record)
-{
-	XLogRecPtr	lsn = record->EndRecPtr;
-	xl_heap_vacuum *xlrec = (xl_heap_vacuum *) XLogRecGetData(record);
-	Buffer		buffer;
-	BlockNumber blkno;
-	XLogRedoAction action;
-
-	/*
-	 * If we have a full-page image, restore it	(without using a cleanup lock)
-	 * and we're done.
-	 */
-	action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, false,
-										   &buffer);
-	if (action == BLK_NEEDS_REDO)
-	{
-		Page		page = (Page) BufferGetPage(buffer);
-		OffsetNumber *nowunused;
-		Size		datalen;
-		OffsetNumber *offnum;
-
-		nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, &datalen);
-
-		/* Shouldn't be a record unless there's something to do */
-		Assert(xlrec->nunused > 0);
-
-		/* Update all now-unused line pointers */
-		offnum = nowunused;
-		for (int i = 0; i < xlrec->nunused; i++)
-		{
-			OffsetNumber off = *offnum++;
-			ItemId		lp = PageGetItemId(page, off);
-
-			Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
-			ItemIdSetUnused(lp);
-		}
-
-		/* Attempt to truncate line pointer array now */
-		PageTruncateLinePointerArray(page);
-
-		PageSetLSN(page, lsn);
-		MarkBufferDirty(buffer);
-	}
-
-	if (BufferIsValid(buffer))
-	{
-		Size		freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
-		RelFileLocator rlocator;
-
-		XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
-
-		UnlockReleaseBuffer(buffer);
-
-		/*
-		 * After vacuuming LP_DEAD items from a page, it's useful to update
-		 * the FSM about it, as it may cause the page become target for
-		 * insertions later even if vacuum decides not to visit it (which is
-		 * possible if gets marked all-visible.)
-		 *
-		 * Do this regardless of a full-page image being applied, since the
-		 * FSM data is not in the page anyway.
-		 */
-		XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
-	}
-}
-
 /*
  * Replay XLOG_HEAP2_VISIBLE record.
  *
@@ -9943,9 +9882,6 @@ heap2_redo(XLogReaderState *record)
 		case XLOG_HEAP2_PRUNE:
 			heap_xlog_prune(record);
 			break;
-		case XLOG_HEAP2_VACUUM:
-			heap_xlog_vacuum(record);
-			break;
 		case XLOG_HEAP2_VISIBLE:
 			heap_xlog_visible(record);
 			break;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 19b50931b90..135fe2dba3e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -601,7 +601,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 		 */
 		if (do_prune)
 		{
-			heap_page_prune_execute(buffer,
+			heap_page_prune_execute(buffer, false,
 									prstate.redirected, prstate.nredirected,
 									prstate.nowdead, prstate.ndead,
 									prstate.nowunused, prstate.nunused);
@@ -668,12 +668,16 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 	OffsetNumber offsets[MaxHeapTuplesPerPage];
 	bool		do_freeze = presult->nfrozen > 0;
 
+	xlrec.flags = 0;
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(relation);
 	xlrec.nredirected = prstate->nredirected;
 	xlrec.ndead = prstate->ndead;
 	xlrec.nunused = prstate->nunused;
 	xlrec.nplans = 0;
 
+	xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
+
 	/*
 	 * The snapshotConflictHorizon for the whole record should be the most
 	 * conservative of all the horizons calculated for any of the possible
@@ -1149,7 +1153,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum)
  * cleanup lock on the buffer.
  */
 void
-heap_page_prune_execute(Buffer buffer,
+heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 						OffsetNumber *redirected, int nredirected,
 						OffsetNumber *nowdead, int ndead,
 						OffsetNumber *nowunused, int nunused)
@@ -1171,6 +1175,7 @@ heap_page_prune_execute(Buffer buffer,
 		ItemId		tolp PG_USED_FOR_ASSERTS_ONLY;
 
 #ifdef USE_ASSERT_CHECKING
+		Assert(!lp_truncate_only);
 
 		/*
 		 * Any existing item that we set as an LP_REDIRECT (any 'from' item)
@@ -1226,6 +1231,7 @@ heap_page_prune_execute(Buffer buffer,
 		ItemId		lp = PageGetItemId(page, off);
 
 #ifdef USE_ASSERT_CHECKING
+		Assert(!lp_truncate_only);
 
 		/*
 		 * An LP_DEAD line pointer must be left behind when the original item
@@ -1259,23 +1265,29 @@ heap_page_prune_execute(Buffer buffer,
 
 #ifdef USE_ASSERT_CHECKING
 
-		/*
-		 * When heap_page_prune_and_freeze() was called, mark_unused_now may
-		 * have been passed as true, which allows would-be LP_DEAD items to be
-		 * made LP_UNUSED instead. This is only possible if the relation has
-		 * no indexes. If there are any dead items, then mark_unused_now was
-		 * not true and every item being marked LP_UNUSED must refer to a
-		 * heap-only tuple.
-		 */
-		if (ndead > 0)
+		if (lp_truncate_only)
 		{
-			Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
-			htup = (HeapTupleHeader) PageGetItem(page, lp);
-			Assert(HeapTupleHeaderIsHeapOnly(htup));
+			/* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
+			Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
 		}
 		else
 		{
-			Assert(ItemIdIsUsed(lp));
+			/*
+			 * When heap_page_prune_and_freeze() was called, mark_unused_now
+			 * may have been passed as true, which allows would-be LP_DEAD
+			 * items to be made LP_UNUSED instead. This is only possible if
+			 * the relation has no indexes. If there are any dead items, then
+			 * mark_unused_now was not true and every item being marked
+			 * LP_UNUSED must refer to a heap-only tuple.
+			 */
+			if (ndead > 0)
+			{
+				Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
+				htup = (HeapTupleHeader) PageGetItem(page, lp);
+				Assert(HeapTupleHeaderIsHeapOnly(htup));
+			}
+			else
+				Assert(ItemIdIsUsed(lp));
 		}
 
 #endif
@@ -1283,17 +1295,22 @@ heap_page_prune_execute(Buffer buffer,
 		ItemIdSetUnused(lp);
 	}
 
-	/*
-	 * Finally, repair any fragmentation, and update the page's hint bit about
-	 * whether it has free pointers.
-	 */
-	PageRepairFragmentation(page);
+	if (lp_truncate_only)
+		PageTruncateLinePointerArray(page);
+	else
+	{
+		/*
+		 * Finally, repair any fragmentation, and update the page's hint bit
+		 * about whether it has free pointers.
+		 */
+		PageRepairFragmentation(page);
 
-	/*
-	 * Now that the page has been modified, assert that redirect items still
-	 * point to valid targets.
-	 */
-	page_verify_redirects(page);
+		/*
+		 * Now that the page has been modified, assert that redirect items
+		 * still point to valid targets.
+		 */
+		page_verify_redirects(page);
+	}
 }
 
 
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c4553a4159c..9dfb56475cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -2394,18 +2394,24 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
 	/* XLOG stuff */
 	if (RelationNeedsWAL(vacrel->rel))
 	{
-		xl_heap_vacuum xlrec;
+		xl_heap_prune xlrec;
 		XLogRecPtr	recptr;
 
+		xlrec.flags = XLHP_LP_TRUNCATE_ONLY;
+		xlrec.snapshotConflictHorizon = InvalidTransactionId;
+		xlrec.nplans = 0;
+		xlrec.nredirected = 0;
+		xlrec.ndead = 0;
 		xlrec.nunused = nunused;
+		xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(vacrel->rel);
 
 		XLogBeginInsert();
-		XLogRegisterData((char *) &xlrec, SizeOfHeapVacuum);
+		XLogRegisterData((char *) &xlrec, SizeOfHeapPrune);
 
 		XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
 		XLogRegisterBufData(0, (char *) unused, nunused * sizeof(OffsetNumber));
 
-		recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);
+		recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_PRUNE);
 
 		PageSetLSN(page, recptr);
 	}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index 9f0a0341d40..ea03f902fc4 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -242,23 +242,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
 			}
 		}
 	}
-	else if (info == XLOG_HEAP2_VACUUM)
-	{
-		xl_heap_vacuum *xlrec = (xl_heap_vacuum *) rec;
-
-		appendStringInfo(buf, "nunused: %u", xlrec->nunused);
-
-		if (XLogRecHasBlockData(record, 0))
-		{
-			OffsetNumber *nowunused;
-
-			nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, NULL);
-
-			appendStringInfoString(buf, ", unused:");
-			array_desc(buf, nowunused, sizeof(OffsetNumber), xlrec->nunused,
-					   &offset_elem_desc, NULL);
-		}
-	}
 	else if (info == XLOG_HEAP2_VISIBLE)
 	{
 		xl_heap_visible *xlrec = (xl_heap_visible *) rec;
@@ -360,9 +343,6 @@ heap2_identify(uint8 info)
 		case XLOG_HEAP2_PRUNE:
 			id = "PRUNE";
 			break;
-		case XLOG_HEAP2_VACUUM:
-			id = "VACUUM";
-			break;
 		case XLOG_HEAP2_VISIBLE:
 			id = "VISIBLE";
 			break;
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index f77051572fd..38d1bdd825e 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -446,7 +446,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 			 * interested in.
 			 */
 		case XLOG_HEAP2_PRUNE:
-		case XLOG_HEAP2_VACUUM:
 		case XLOG_HEAP2_VISIBLE:
 		case XLOG_HEAP2_LOCK_UPDATED:
 			break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 321a46185e1..d5cb8f99cac 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -349,7 +349,7 @@ extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 									   HeapPageFreeze *pagefrz,
 									   PruneFreezeResult *presult,
 									   OffsetNumber *off_loc);
-extern void heap_page_prune_execute(Buffer buffer,
+extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 									OffsetNumber *redirected, int nredirected,
 									OffsetNumber *nowdead, int ndead,
 									OffsetNumber *nowunused, int nunused);
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index fe4a8ff0620..2393540cf68 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -52,11 +52,10 @@
  */
 #define XLOG_HEAP2_REWRITE		0x00
 #define XLOG_HEAP2_PRUNE		0x10
-#define XLOG_HEAP2_VACUUM		0x20
-#define XLOG_HEAP2_VISIBLE		0x30
-#define XLOG_HEAP2_MULTI_INSERT 0x40
-#define XLOG_HEAP2_LOCK_UPDATED 0x50
-#define XLOG_HEAP2_NEW_CID		0x60
+#define XLOG_HEAP2_VISIBLE		0x20
+#define XLOG_HEAP2_MULTI_INSERT 0x30
+#define XLOG_HEAP2_LOCK_UPDATED 0x40
+#define XLOG_HEAP2_NEW_CID		0x50
 
 /*
  * xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
@@ -266,6 +265,7 @@ typedef struct xl_heap_freeze_plan
  */
 typedef struct xl_heap_prune
 {
+	uint8		flags;
 	TransactionId snapshotConflictHorizon;
 	uint16		nplans;
 	uint16		nredirected;
@@ -288,19 +288,22 @@ typedef struct xl_heap_prune
 
 #define SizeOfHeapPrune (offsetof(xl_heap_prune, isCatalogRel) + sizeof(bool))
 
+/* Flags for xl_heap_prune */
+
 /*
- * The vacuum page record is similar to the prune record, but can only mark
- * already LP_DEAD items LP_UNUSED (during VACUUM's second heap pass)
- *
- * Acquires an ordinary exclusive lock only.
+ * During vacuum's second pass which sets LP_DEAD items LP_UNUSED, we will only
+ * truncate the line pointer array, not call PageRepairFragmentation. We need
+ * this flag to differentiate what kind of lock (exclusive or cleanup) to take
+ * on the buffer and whether to call PageTruncateLinePointerArray() or
+ * PageRepairFragementation().
  */
-typedef struct xl_heap_vacuum
-{
-	uint16		nunused;
-	/* OFFSET NUMBERS are in the block reference 0 */
-} xl_heap_vacuum;
+#define		XLHP_LP_TRUNCATE_ONLY       (1 << 1)
 
-#define SizeOfHeapVacuum (offsetof(xl_heap_vacuum, nunused) + sizeof(uint16))
+/*
+ * Vacuum's first pass and on-access pruning may need to include a snapshot
+ * conflict horizon.
+ */
+#define		XLHP_HAS_CONFLICT_HORIZON   (1 << 2)
 
 /* flags for infobits_set */
 #define XLHL_XMAX_IS_MULTI		0x01
-- 
2.40.1


--tez7m2a73jtztiij
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0015-Set-hastup-in-heap_page_prune.patch"



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* MERGE behavior with REPEATABLE READ isolation level
@ 2026-02-15 01:20 Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Alexander Korotkov @ 2026-02-15 01:20 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>

Hi hackers,

I found it strange that ExecMergeMatched() checks for
IsolationUsesXactSnapshot() in the TM_Deleted case, but not in the
TM_Updated case.  Indeed, EPQ works on the repeatable read isolation level!

s1# create table test (id int primary key, val int);
s1# insert into test values (1,0);

s2# begin;
s2# update test set val = val + 100;

s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
      ON t.id = s.id
    WHEN MATCHED THEN
      UPDATE SET val = t.val + s.inc
    WHEN NOT MATCHED THEN
      INSERT (id, val) VALUES (s.id, s.inc);
(waiting ...)

s2# commit;
s1# MERGE 1
s1# select * from test;
 id | val
----+-----
  1 | 200
(1 row)

Quick search didn't give an explanation of this to me.  I also don't see
this covered by an isolation test.  Is it an intended behavior or a bug?
(Sorry for buzz if this was discussed before)

------
Regards,
Alexander Korotkov
Supabase

^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: MERGE behavior with REPEATABLE READ isolation level
  2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
@ 2026-02-15 01:22 ` Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-24 03:12   ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Alexander Korotkov @ 2026-02-15 01:22 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>

On Sun, Feb 15, 2026 at 3:20 AM Alexander Korotkov <aekorotkov@gmail.com>
wrote:

> Hi hackers,
>
> I found it strange that ExecMergeMatched() checks for
> IsolationUsesXactSnapshot() in the TM_Deleted case, but not in the
> TM_Updated case.  Indeed, EPQ works on the repeatable read isolation level!
>
> s1# create table test (id int primary key, val int);
> s1# insert into test values (1,0);
>
> s2# begin;
> s2# update test set val = val + 100;
>
> s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
>       ON t.id = s.id
>     WHEN MATCHED THEN
>       UPDATE SET val = t.val + s.inc
>     WHEN NOT MATCHED THEN
>       INSERT (id, val) VALUES (s.id, s.inc);
> (waiting ...)
>
> s2# commit;
> s1# MERGE 1
> s1# select * from test;
>  id | val
> ----+-----
>   1 | 200
> (1 row)
>

Oh, sorry I missed the begin statement for s1.  The complete case should
look like this.

s1# create table test (id int primary key, val int);
s1# insert into test values (1,0);

  s2# begin;
  s2# update test set val = val + 100;

s1# begin isolation level repeatable read;
s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
      ON t.id = s.id
    WHEN MATCHED THEN
      UPDATE SET val = t.val + s.inc
    WHEN NOT MATCHED THEN
      INSERT (id, val) VALUES (s.id, s.inc);
(waiting ...)

  s2# commit;

s1# MERGE 1
s1# select * from test;
 id | val
----+-----
  1 | 200
(1 row)

------
Regards,
Alexander Korotkov
Supabase

^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: MERGE behavior with REPEATABLE READ isolation level
  2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
@ 2026-02-24 03:12   ` Tender Wang <tndrwang@gmail.com>
  2026-02-24 09:09     ` Re: MERGE behavior with REPEATABLE READ isolation level Dean Rasheed <dean.a.rasheed@gmail.com>
  2026-02-26 17:03     ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  0 siblings, 2 replies; 10+ messages in thread

From: Tender Wang @ 2026-02-24 03:12 UTC (permalink / raw)
  To: Alexander Korotkov <aekorotkov@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>

Hi Alexander,

Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月15日周日 09:23写道:
> Oh, sorry I missed the begin statement for s1.  The complete case should look like this.
>
> s1# create table test (id int primary key, val int);
> s1# insert into test values (1,0);
>
>   s2# begin;
>   s2# update test set val = val + 100;
>
> s1# begin isolation level repeatable read;
> s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
>       ON t.id = s.id
>     WHEN MATCHED THEN
>       UPDATE SET val = t.val + s.inc
>     WHEN NOT MATCHED THEN
>       INSERT (id, val) VALUES (s.id, s.inc);
> (waiting ...)
>
>   s2# commit;
>
> s1# MERGE 1
> s1# select * from test;
>  id | val
> ----+-----
>   1 | 200
> (1 row)
>

I tried "update test set val = val + 100;" but the SQL reported a
"could not serialize access due to concurrent update" error.
It seems that the MERGE command should behave identically to UPDATE
when performing a match action.

I wrote a fix patch and attached it, and added your test case, too.


-- 
Thanks,
Tender Wang
From 9f7267cdc3bf5b34eb95686f288d39b5766142ac Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH] Fix MERGE match do update in RR isolation level.

---
 src/backend/executor/nodeModifyTable.c        |  4 ++
 .../expected/merge-match-do-update.out        | 26 +++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/merge-match-do-update.spec          | 43 +++++++++++++++++++
 4 files changed, 74 insertions(+)
 create mode 100644 src/test/isolation/expected/merge-match-do-update.out
 create mode 100644 src/test/isolation/specs/merge-match-do-update.spec

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
 							   *inputslot;
 					LockTupleMode lockmode;
 
+					if (IsolationUsesXactSnapshot())
+						ereport(ERROR,
+								(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+								 errmsg("could not serialize access due to concurrent update")));
 					/*
 					 * The target tuple was concurrently updated by some other
 					 * transaction.  If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-match-do-update.out b/src/test/isolation/expected/merge-match-do-update.out
new file mode 100644
index 00000000000..c2cde0de606
--- /dev/null
+++ b/src/test/isolation/expected/merge-match-do-update.out
@@ -0,0 +1,26 @@
+Parsed test spec with 2 sessions
+
+starting permutation: b2 update2 b1 merge1 c2 select1 c1
+step b2: BEGIN;
+step update2: 
+  UPDATE test SET val = val + 100;
+
+step b1: 
+  BEGIN ISOLATION LEVEL REPEATABLE READ;
+
+step merge1: 
+  MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+      ON t.id = s.id
+    WHEN MATCHED THEN
+      UPDATE SET val = t.val + s.inc
+    WHEN NOT MATCHED THEN
+      INSERT (id, val) VALUES (s.id, s.inc);
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step select1: 
+  SELECT * FROM test;
+
+ERROR:  current transaction is aborted, commands ignored until end of transaction block
+step c1: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..95d8597e204 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -58,6 +58,7 @@ test: insert-conflict-do-select
 test: merge-insert-update
 test: merge-delete
 test: merge-update
+test: merge-match-do-update
 test: merge-match-recheck
 test: merge-join
 test: delete-abort-savept
diff --git a/src/test/isolation/specs/merge-match-do-update.spec b/src/test/isolation/specs/merge-match-do-update.spec
new file mode 100644
index 00000000000..046c78826b9
--- /dev/null
+++ b/src/test/isolation/specs/merge-match-do-update.spec
@@ -0,0 +1,43 @@
+# MERGE UPDATE
+
+setup
+{
+  CREATE TABLE test (id int primary key, val int);
+  INSERT INTO test VALUES (1,0);
+}
+
+teardown
+{
+  DROP TABLE test;
+}
+
+session "s1"
+step "b1"
+{
+  BEGIN ISOLATION LEVEL REPEATABLE READ;
+}
+step "merge1"
+{
+  MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+      ON t.id = s.id
+    WHEN MATCHED THEN
+      UPDATE SET val = t.val + s.inc
+    WHEN NOT MATCHED THEN
+      INSERT (id, val) VALUES (s.id, s.inc);
+}
+step "select1"
+{
+  SELECT * FROM test;
+}
+step "c1" { COMMIT; }
+
+session "s2"
+step "b2" { BEGIN; }
+step "update2"
+{
+  UPDATE test SET val = val + 100;
+}
+step "c2" { COMMIT; }
+
+# Now with concurrency
+permutation "b2" "update2" "b1" "merge1" "c2" "select1" "c1"
\ No newline at end of file
-- 
2.34.1



Attachments:

  [text/plain] 0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch (3.7K, ../../CAHewXNmtg=-WRczRUmd0tyNaP57WYOg74=O2pMssq9hWS3YoOw@mail.gmail.com/2-0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch)
  download | inline diff:
From 9f7267cdc3bf5b34eb95686f288d39b5766142ac Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH] Fix MERGE match do update in RR isolation level.

---
 src/backend/executor/nodeModifyTable.c        |  4 ++
 .../expected/merge-match-do-update.out        | 26 +++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/merge-match-do-update.spec          | 43 +++++++++++++++++++
 4 files changed, 74 insertions(+)
 create mode 100644 src/test/isolation/expected/merge-match-do-update.out
 create mode 100644 src/test/isolation/specs/merge-match-do-update.spec

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
 							   *inputslot;
 					LockTupleMode lockmode;
 
+					if (IsolationUsesXactSnapshot())
+						ereport(ERROR,
+								(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+								 errmsg("could not serialize access due to concurrent update")));
 					/*
 					 * The target tuple was concurrently updated by some other
 					 * transaction.  If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-match-do-update.out b/src/test/isolation/expected/merge-match-do-update.out
new file mode 100644
index 00000000000..c2cde0de606
--- /dev/null
+++ b/src/test/isolation/expected/merge-match-do-update.out
@@ -0,0 +1,26 @@
+Parsed test spec with 2 sessions
+
+starting permutation: b2 update2 b1 merge1 c2 select1 c1
+step b2: BEGIN;
+step update2: 
+  UPDATE test SET val = val + 100;
+
+step b1: 
+  BEGIN ISOLATION LEVEL REPEATABLE READ;
+
+step merge1: 
+  MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+      ON t.id = s.id
+    WHEN MATCHED THEN
+      UPDATE SET val = t.val + s.inc
+    WHEN NOT MATCHED THEN
+      INSERT (id, val) VALUES (s.id, s.inc);
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step select1: 
+  SELECT * FROM test;
+
+ERROR:  current transaction is aborted, commands ignored until end of transaction block
+step c1: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..95d8597e204 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -58,6 +58,7 @@ test: insert-conflict-do-select
 test: merge-insert-update
 test: merge-delete
 test: merge-update
+test: merge-match-do-update
 test: merge-match-recheck
 test: merge-join
 test: delete-abort-savept
diff --git a/src/test/isolation/specs/merge-match-do-update.spec b/src/test/isolation/specs/merge-match-do-update.spec
new file mode 100644
index 00000000000..046c78826b9
--- /dev/null
+++ b/src/test/isolation/specs/merge-match-do-update.spec
@@ -0,0 +1,43 @@
+# MERGE UPDATE
+
+setup
+{
+  CREATE TABLE test (id int primary key, val int);
+  INSERT INTO test VALUES (1,0);
+}
+
+teardown
+{
+  DROP TABLE test;
+}
+
+session "s1"
+step "b1"
+{
+  BEGIN ISOLATION LEVEL REPEATABLE READ;
+}
+step "merge1"
+{
+  MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+      ON t.id = s.id
+    WHEN MATCHED THEN
+      UPDATE SET val = t.val + s.inc
+    WHEN NOT MATCHED THEN
+      INSERT (id, val) VALUES (s.id, s.inc);
+}
+step "select1"
+{
+  SELECT * FROM test;
+}
+step "c1" { COMMIT; }
+
+session "s2"
+step "b2" { BEGIN; }
+step "update2"
+{
+  UPDATE test SET val = val + 100;
+}
+step "c2" { COMMIT; }
+
+# Now with concurrency
+permutation "b2" "update2" "b1" "merge1" "c2" "select1" "c1"
\ No newline at end of file
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: MERGE behavior with REPEATABLE READ isolation level
  2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-24 03:12   ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
@ 2026-02-24 09:09     ` Dean Rasheed <dean.a.rasheed@gmail.com>
  1 sibling, 0 replies; 10+ messages in thread

From: Dean Rasheed @ 2026-02-24 09:09 UTC (permalink / raw)
  To: Tender Wang <tndrwang@gmail.com>; +Cc: Alexander Korotkov <aekorotkov@gmail.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>

On Tue, 24 Feb 2026 at 03:13, Tender Wang <tndrwang@gmail.com> wrote:
>
> Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月15日周日 09:23写道:
> >
> > I found it strange that ExecMergeMatched() checks for IsolationUsesXactSnapshot() in the TM_Deleted case, but not in the TM_Updated case.
>
> I tried "update test set val = val + 100;" but the SQL reported a
> "could not serialize access due to concurrent update" error.
> It seems that the MERGE command should behave identically to UPDATE
> when performing a match action.
>

Yes, I agree. I think this is a bug (probably just an oversight in the
original MERGE commit).

Regards,
Dean





^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: MERGE behavior with REPEATABLE READ isolation level
  2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-24 03:12   ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
@ 2026-02-26 17:03     ` Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-27 12:54       ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
  1 sibling, 1 reply; 10+ messages in thread

From: Alexander Korotkov @ 2026-02-26 17:03 UTC (permalink / raw)
  To: Tender Wang <tndrwang@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>

On Tue, Feb 24, 2026 at 5:13 AM Tender Wang <tndrwang@gmail.com> wrote:
> Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月15日周日 09:23写道:
> > Oh, sorry I missed the begin statement for s1.  The complete case should look like this.
> >
> > s1# create table test (id int primary key, val int);
> > s1# insert into test values (1,0);
> >
> >   s2# begin;
> >   s2# update test set val = val + 100;
> >
> > s1# begin isolation level repeatable read;
> > s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
> >       ON t.id = s.id
> >     WHEN MATCHED THEN
> >       UPDATE SET val = t.val + s.inc
> >     WHEN NOT MATCHED THEN
> >       INSERT (id, val) VALUES (s.id, s.inc);
> > (waiting ...)
> >
> >   s2# commit;
> >
> > s1# MERGE 1
> > s1# select * from test;
> >  id | val
> > ----+-----
> >   1 | 200
> > (1 row)
> >
>
> I tried "update test set val = val + 100;" but the SQL reported a
> "could not serialize access due to concurrent update" error.
> It seems that the MERGE command should behave identically to UPDATE
> when performing a match action.
>
> I wrote a fix patch and attached it, and added your test case, too.

Thank you for the confirmation and for the patch.  Regarding the test
case, can we handle this without introducing a new .spec file?  I
think we can add 1-2 permutations to merge-update.spec.  Even existing
step should work, you only need one new which begins transaction in RR
isolation mode.

------
Regards,
Alexander Korotkov
Supabase





^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: MERGE behavior with REPEATABLE READ isolation level
  2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-24 03:12   ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
  2026-02-26 17:03     ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
@ 2026-02-27 12:54       ` Tender Wang <tndrwang@gmail.com>
  2026-03-01 22:32         ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Tender Wang @ 2026-02-27 12:54 UTC (permalink / raw)
  To: Alexander Korotkov <aekorotkov@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Dean Rasheed <dean.a.rasheed@gmail.com>

Hi all,

Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月27日周五 01:03写道:
> Thank you for the confirmation and for the patch.  Regarding the test
> case, can we handle this without introducing a new .spec file?  I
> think we can add 1-2 permutations to merge-update.spec.  Even existing
> step should work, you only need one new which begins transaction in RR
> isolation mode.

Done
Please see the v2 patch.

-- 
Thanks,
Tender Wang
From 6a8dadb846338648e289ae3ac7e90eba53e35564 Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH v2] Fix MERGE match do update in RR isolation level.

---
 src/backend/executor/nodeModifyTable.c       |  4 +++
 src/test/isolation/expected/merge-update.out | 33 ++++++++++++++++++++
 src/test/isolation/specs/merge-update.spec   |  2 ++
 3 files changed, 39 insertions(+)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
 							   *inputslot;
 					LockTupleMode lockmode;
 
+					if (IsolationUsesXactSnapshot())
+						ereport(ERROR,
+								(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+								 errmsg("could not serialize access due to concurrent update")));
 					/*
 					 * The target tuple was concurrently updated by some other
 					 * transaction.  If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out
index feceacf4818..821565b4303 100644
--- a/src/test/isolation/expected/merge-update.out
+++ b/src/test/isolation/expected/merge-update.out
@@ -549,3 +549,36 @@ step c1: COMMIT;
 step pa_merge2c_dup: <... completed>
 ERROR:  MERGE command cannot affect row a second time
 step a2: ABORT;
+
+starting permutation: merge2a c1 s1beginrr merge1 c2
+step merge2a: 
+  MERGE INTO target t
+  USING (SELECT 1 as key, 'merge2a' as val) s
+  ON s.key = t.key
+  WHEN NOT MATCHED THEN
+	INSERT VALUES (s.key, s.val)
+  WHEN MATCHED THEN
+	UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val
+  WHEN NOT MATCHED BY SOURCE THEN
+	UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a'
+  RETURNING merge_action(), old, new, t.*;
+
+merge_action|old       |new                            |key|val                      
+------------+----------+-------------------------------+---+-------------------------
+UPDATE      |(1,setup1)|(2,"setup1 updated by merge2a")|  2|setup1 updated by merge2a
+(1 row)
+
+step c1: COMMIT;
+step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step merge1: 
+  MERGE INTO target t
+  USING (SELECT 1 as key, 'merge1' as val) s
+  ON s.key = t.key
+  WHEN NOT MATCHED THEN
+	INSERT VALUES (s.key, s.val)
+  WHEN MATCHED THEN
+    UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR:  could not serialize access due to concurrent update
diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec
index 771ee5b70cf..b902779edd6 100644
--- a/src/test/isolation/specs/merge-update.spec
+++ b/src/test/isolation/specs/merge-update.spec
@@ -93,6 +93,7 @@ step "pa_merge3"
 }
 step "c1" { COMMIT; }
 step "a1" { ABORT; }
+step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
 
 session "s2"
 setup
@@ -223,3 +224,4 @@ permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds
 permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple
 permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple
 permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2"
+permutation "merge2a" "c1" "s1beginrr" "merge1" "c2"
-- 
2.34.1



Attachments:

  [text/plain] v2-0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch (3.4K, ../../CAHewXNk+LhHOUE7EhugnkanEJABp8sg4ViXr4L80LZrQvyuBqg@mail.gmail.com/2-v2-0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch)
  download | inline diff:
From 6a8dadb846338648e289ae3ac7e90eba53e35564 Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH v2] Fix MERGE match do update in RR isolation level.

---
 src/backend/executor/nodeModifyTable.c       |  4 +++
 src/test/isolation/expected/merge-update.out | 33 ++++++++++++++++++++
 src/test/isolation/specs/merge-update.spec   |  2 ++
 3 files changed, 39 insertions(+)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
 							   *inputslot;
 					LockTupleMode lockmode;
 
+					if (IsolationUsesXactSnapshot())
+						ereport(ERROR,
+								(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+								 errmsg("could not serialize access due to concurrent update")));
 					/*
 					 * The target tuple was concurrently updated by some other
 					 * transaction.  If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out
index feceacf4818..821565b4303 100644
--- a/src/test/isolation/expected/merge-update.out
+++ b/src/test/isolation/expected/merge-update.out
@@ -549,3 +549,36 @@ step c1: COMMIT;
 step pa_merge2c_dup: <... completed>
 ERROR:  MERGE command cannot affect row a second time
 step a2: ABORT;
+
+starting permutation: merge2a c1 s1beginrr merge1 c2
+step merge2a: 
+  MERGE INTO target t
+  USING (SELECT 1 as key, 'merge2a' as val) s
+  ON s.key = t.key
+  WHEN NOT MATCHED THEN
+	INSERT VALUES (s.key, s.val)
+  WHEN MATCHED THEN
+	UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val
+  WHEN NOT MATCHED BY SOURCE THEN
+	UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a'
+  RETURNING merge_action(), old, new, t.*;
+
+merge_action|old       |new                            |key|val                      
+------------+----------+-------------------------------+---+-------------------------
+UPDATE      |(1,setup1)|(2,"setup1 updated by merge2a")|  2|setup1 updated by merge2a
+(1 row)
+
+step c1: COMMIT;
+step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step merge1: 
+  MERGE INTO target t
+  USING (SELECT 1 as key, 'merge1' as val) s
+  ON s.key = t.key
+  WHEN NOT MATCHED THEN
+	INSERT VALUES (s.key, s.val)
+  WHEN MATCHED THEN
+    UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR:  could not serialize access due to concurrent update
diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec
index 771ee5b70cf..b902779edd6 100644
--- a/src/test/isolation/specs/merge-update.spec
+++ b/src/test/isolation/specs/merge-update.spec
@@ -93,6 +93,7 @@ step "pa_merge3"
 }
 step "c1" { COMMIT; }
 step "a1" { ABORT; }
+step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
 
 session "s2"
 setup
@@ -223,3 +224,4 @@ permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds
 permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple
 permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple
 permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2"
+permutation "merge2a" "c1" "s1beginrr" "merge1" "c2"
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: MERGE behavior with REPEATABLE READ isolation level
  2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-24 03:12   ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
  2026-02-26 17:03     ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
  2026-02-27 12:54       ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
@ 2026-03-01 22:32         ` Alexander Korotkov <aekorotkov@gmail.com>
  0 siblings, 0 replies; 10+ messages in thread

From: Alexander Korotkov @ 2026-03-01 22:32 UTC (permalink / raw)
  To: Tender Wang <tndrwang@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Dean Rasheed <dean.a.rasheed@gmail.com>

On Fri, Feb 27, 2026 at 2:54 PM Tender Wang <tndrwang@gmail.com> wrote:
> Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月27日周五 01:03写道:
> > Thank you for the confirmation and for the patch.  Regarding the test
> > case, can we handle this without introducing a new .spec file?  I
> > think we can add 1-2 permutations to merge-update.spec.  Even existing
> > step should work, you only need one new which begins transaction in RR
> > isolation mode.
>
> Done
> Please see the v2 patch.

Looks good to me.  I'll push it if no objections.

------
Regards,
Alexander Korotkov
Supabase





^ permalink  raw  reply  [nested|flat] 10+ messages in thread


end of thread, other threads:[~2026-03-01 22:32 UTC | newest]

Thread overview: 10+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-03-19 22:50 [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record Melanie Plageman <melanieplageman@gmail.com>
2024-03-19 22:50 [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record Melanie Plageman <melanieplageman@gmail.com>
2024-03-19 22:50 [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record Melanie Plageman <melanieplageman@gmail.com>
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12   ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
2026-02-24 09:09     ` Re: MERGE behavior with REPEATABLE READ isolation level Dean Rasheed <dean.a.rasheed@gmail.com>
2026-02-26 17:03     ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-27 12:54       ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
2026-03-01 22:32         ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox