From 8f9be07a43d2d68e1217888678be5d383380f0e8 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 4 Nov 2026 20:38:12 +0200
Subject: [PATCH 1/1] Fix WAL-logging init fork (for unlogged rels) with
 generic WAL

The generic WAL code skips emitting WAL records for unlogged
relations, but it only considered RelationNeedsWAL() and didn't take
into account that the init fork of an unlogged relation does need to
be WAL-logged.  That meant that WAL-logging the init fork from
ambuildempty() using generic WAL did not work.

Since commit ccadf73163c, contrib/bloom's blbuildempty() uses generic
WAL for the metapage, so it's been broken ever since.  Backpatch to
v16 like that commit.  Earlier versions have the same problem, but
since there's no (core) code using generic WAL from ambuildempty()
before that, it doesn't seem worth the risk backpatching further.

Co-authored-by: Narek Galstyan <narekg@berkeley.edu>
Discussion: https://www.postgresql.org/message-id/CAHsn6_y9zCAj5Vc4-nOQrNu3io_ToVdMJkbgAfW-wOnuwF6Bmw@mail.gmail.com
Backpatch-through: 16
---
 contrib/bloom/t/001_wal.pl                | 22 ++++++
 src/backend/access/transam/generic_xlog.c | 90 ++++++++++++++---------
 2 files changed, 78 insertions(+), 34 deletions(-)

diff --git a/contrib/bloom/t/001_wal.pl b/contrib/bloom/t/001_wal.pl
index 683b1876055..27864495243 100644
--- a/contrib/bloom/t/001_wal.pl
+++ b/contrib/bloom/t/001_wal.pl
@@ -81,4 +81,26 @@ for my $i (1 .. 10)
 	test_index_replay("insert $i");
 }
 
+# Also test an unlogged index. (We had bugs in WAL-logging the init
+# fork in the past.)
+$node_primary->safe_psql("postgres",
+	"CREATE UNLOGGED TABLE tst_unlogged (i int4, t text);");
+$node_primary->safe_psql("postgres",
+	"CREATE INDEX bloomidx_unlogged ON tst_unlogged USING bloom (i, t);");
+$node_primary->wait_for_catchup($node_standby);
+
+my $unlogged_queries = qq(INSERT INTO tst_unlogged VALUES (1, 'a');
+SET enable_seqscan=off;
+SELECT count(*) FROM tst_unlogged WHERE i = 1;
+);
+
+$node_primary->stop('immediate');
+$node_primary->start;
+is($node_primary->safe_psql("postgres", $unlogged_queries),
+	'1', 'unlogged index usable after crash recovery');
+
+$node_standby->promote;
+is($node_standby->safe_psql("postgres", $unlogged_queries),
+	'1', 'unlogged index usable after standby promotion');
+
 done_testing();
diff --git a/src/backend/access/transam/generic_xlog.c b/src/backend/access/transam/generic_xlog.c
index 7f82186d0d6..ac7caaeb138 100644
--- a/src/backend/access/transam/generic_xlog.c
+++ b/src/backend/access/transam/generic_xlog.c
@@ -67,6 +67,7 @@ struct GenericXLogState
 	PGIOAlignedBlock images[MAX_GENERIC_XLOG_PAGES];
 	/* Info about each page, see above */
 	GenericXLogPageData pages[MAX_GENERIC_XLOG_PAGES];
+	/* RelationNeedsWAL() result */
 	bool		isLogged;
 };
 
@@ -331,36 +332,59 @@ GenericXLogRegisterBuffer(GenericXLogState *state, Buffer buffer, int flags)
 
 /*
  * Apply changes represented by GenericXLogState to the actual buffers,
- * and emit a generic xlog record.
+ * and emit a generic xlog record (if needed).
  */
 XLogRecPtr
 GenericXLogFinish(GenericXLogState *state)
 {
 	XLogRecPtr	lsn;
 	int			i;
+	bool		logged[MAX_GENERIC_XLOG_PAGES];
+	bool		loggedAny = false;
 
-	if (state->isLogged)
+	START_CRIT_SECTION();
+
+	/*
+	 * Compute deltas if necessary, write changes to buffers, mark buffers
+	 * dirty, and register changes.
+	 */
+	for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++)
 	{
-		/* Logged relation: make xlog record in critical section. */
-		XLogBeginInsert();
+		GenericXLogPageData *pageData = &state->pages[i];
+		bool		xlogNeeded;
 
-		START_CRIT_SECTION();
+		if (BufferIsInvalid(pageData->buffer))
+		{
+			logged[i] = false;
+			continue;
+		}
 
-		/*
-		 * Compute deltas if necessary, write changes to buffers, mark buffers
-		 * dirty, and register changes.
-		 */
-		for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++)
+		/* Does this page need to be WAL-logged? */
+		if (state->isLogged)
+			xlogNeeded = true;
+		else
 		{
-			GenericXLogPageData *pageData = &state->pages[i];
-			Page		page;
-			PageHeader	pageHeader;
+			RelFileLocator rlocator;
+			ForkNumber	forknum;
+			BlockNumber blkno;
 
-			if (BufferIsInvalid(pageData->buffer))
-				continue;
+			BufferGetTag(pageData->buffer, &rlocator, &forknum, &blkno);
 
-			page = BufferGetPage(pageData->buffer);
-			pageHeader = (PageHeader) pageData->image;
+			xlogNeeded = (forknum == INIT_FORKNUM);
+		}
+		logged[i] = xlogNeeded;
+
+		/* call XLogBeginInsert() before starting to build the WAL record */
+		if (xlogNeeded && !loggedAny)
+		{
+			XLogBeginInsert();
+			loggedAny = true;
+		}
+
+		if (xlogNeeded)
+		{
+			Page		page = BufferGetPage(pageData->buffer);
+			PageHeader	pageHeader;
 
 			/*
 			 * Compute delta while we still have both the unmodified page and
@@ -374,6 +398,7 @@ GenericXLogFinish(GenericXLogState *state)
 			 * pd_lower and pd_upper in order to avoid divergence between
 			 * actual page state and what replay would produce.
 			 */
+			pageHeader = (PageHeader) pageData->image;
 			memcpy(page, pageData->image, pageHeader->pd_lower);
 			memset(page + pageHeader->pd_lower, 0,
 				   pageHeader->pd_upper - pageHeader->pd_lower);
@@ -394,7 +419,18 @@ GenericXLogFinish(GenericXLogState *state)
 				XLogRegisterBufData(i, pageData->delta, pageData->deltaLen);
 			}
 		}
+		else
+		{
+			memcpy(BufferGetPage(pageData->buffer),
+				   pageData->image,
+				   BLCKSZ);
+			/* We don't worry about zeroing the "hole" in this case */
+			MarkBufferDirty(pageData->buffer);
+		}
+	}
 
+	if (loggedAny)
+	{
 		/* Insert xlog record */
 		lsn = XLogInsert(RM_GENERIC_ID, 0);
 
@@ -403,32 +439,18 @@ GenericXLogFinish(GenericXLogState *state)
 		{
 			GenericXLogPageData *pageData = &state->pages[i];
 
-			if (BufferIsInvalid(pageData->buffer))
+			if (!logged[i])
 				continue;
+
 			PageSetLSN(BufferGetPage(pageData->buffer), lsn);
 		}
-		END_CRIT_SECTION();
 	}
 	else
 	{
-		/* Unlogged relation: skip xlog-related stuff */
-		START_CRIT_SECTION();
-		for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++)
-		{
-			GenericXLogPageData *pageData = &state->pages[i];
-
-			if (BufferIsInvalid(pageData->buffer))
-				continue;
-			memcpy(BufferGetPage(pageData->buffer),
-				   pageData->image,
-				   BLCKSZ);
-			/* We don't worry about zeroing the "hole" in this case */
-			MarkBufferDirty(pageData->buffer);
-		}
-		END_CRIT_SECTION();
 		/* We don't have a LSN to return, in this case */
 		lsn = InvalidXLogRecPtr;
 	}
+	END_CRIT_SECTION();
 
 	pfree(state);
 
-- 
2.47.3

