From b54e3dbf7455333a941e2b16a49520b02f7d8b52 Mon Sep 17 00:00:00 2001
From: Satya Narlapuram <satyanarlapuram@gmail.com>
Date: Fri, 1 May 2026 15:37:51 +0000
Subject: [PATCH] Skip WAL for unlogged relations during online checksum enable

ProcessSingleRelationFork() unconditionally called log_newpage_buffer()
for every page of every relation during pg_enable_data_checksums().
This included unlogged relations, which by definition never generate
WAL for data changes and are reset to their init fork on any recovery.

FPI WAL records are wasteful on the primary (proportional
to the total size of all unlogged tables) and cause the standby to
pointlessly materialize data files that should not exist there.

Guard the log_newpage_buffer() call with RelationNeedsWAL() so that
unlogged relations still get their pages dirtied (ensuring the checksum
is flushed to disk at the next checkpoint) but do not emit WAL.

Add a test to 003_standby_restarts.pl that creates an unlogged table,
enables checksums, and verifies via pg_relation_size() on the standby
that no data was materialized for the unlogged relation after replay.
---
 src/backend/postmaster/datachecksum_state.c   |  9 +++-
 .../test_checksums/t/003_standby_restarts.pl  | 45 +++++++++++++++++++
 2 files changed, 52 insertions(+), 2 deletions(-)

diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index d0d6acdd..535da8b0 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -690,11 +690,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 		 * at one point in the past, so only when checksums are first on, then
 		 * off, and then turned on again.  TODO: investigate if this could be
 		 * avoided if the checksum is calculated to be correct and wal_level
-		 * is set to "minimal",
+		 * is set to "minimal".
+		 *
+		 * Unlogged relations don't need WAL since they are reset to their
+		 * init fork on recovery.  We still dirty the buffer so that the
+		 * checksum is written to disk at the next checkpoint.
 		 */
 		START_CRIT_SECTION();
 		MarkBufferDirty(buf);
-		log_newpage_buffer(buf, false);
+		if (RelationNeedsWAL(reln))
+			log_newpage_buffer(buf, false);
 		END_CRIT_SECTION();
 
 		UnlockReleaseBuffer(buf);
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index 11e15c9d..858329b6 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -115,6 +115,51 @@ $result =
   $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
 is($result, "19998", 'ensure we can safely read all data without checksums');
 
+# ---------------------------------------------------------------------------
+# Test that enabling checksums does not emit WAL for unlogged relations.
+# Unlogged relations are wiped on recovery, so FPIs for them would be
+# pointless and waste WAL traffic / standby I/O.
+#
+
+$node_primary->safe_psql('postgres',
+	"CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,10000) AS a;");
+$node_primary->wait_for_catchup($node_standby, 'replay',
+	$node_primary->lsn('insert'));
+
+# Get the relfilenode and database OID so we can search the WAL for it
+my $unlogged_rfn = $node_primary->safe_psql('postgres',
+	"SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';");
+my $db_oid = $node_primary->safe_psql('postgres',
+	"SELECT oid FROM pg_database WHERE datname = 'postgres';");
+
+# Verify the standby only has the init fork (no main fork)
+my $standby_datadir = $node_standby->data_dir;
+ok(!-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+	'standby has no main fork for unlogged table before enable');
+
+# Re-enable data checksums
+enable_data_checksums($node_primary, wait => 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+# After standby replays, the unlogged main file must still not exist.
+# If the bug were present, FPI replay would materialize the full table.
+$node_primary->wait_for_catchup($node_standby, 'replay',
+	$node_primary->lsn('insert'));
+ok(!-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+	'standby has no main fork for unlogged table after enable');
+
+# Verify unlogged relation size is 0 on the standby (main fork missing)
+my $standby_size = $node_standby->safe_psql('postgres',
+	"SELECT pg_relation_size('unlogged_tbl', 'main');");
+is($standby_size, '0',
+	'unlogged table has zero size on standby after checksum enable');
+
+# Unlogged table should still be readable on primary
+$result = $node_primary->safe_psql('postgres',
+	'SELECT count(*) FROM unlogged_tbl;');
+is($result, '10000',
+	'unlogged table readable on primary after checksum enable');
+
 $node_standby->stop;
 $node_primary->stop;
 done_testing();
-- 
2.43.0

