From 557b22e931233e336704d04defee2e19c7706d1c Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Fri, 7 Nov 2025 17:21:26 +0200
Subject: [PATCH 1/2] Add test for vacuuming at multixid wraparound

This currently fails. The next commit fixes the failure.

This isn't fully polished, and I'm not sure if it's worth committing.
---
 src/test/modules/test_misc/meson.build        |   1 +
 .../test_misc/t/010_mxid_wraparound.pl        | 123 ++++++++++++++++++
 2 files changed, 124 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/010_mxid_wraparound.pl

diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index f258bf1ccd9..cf57ed21dc6 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -18,6 +18,7 @@ tests += {
       't/007_catcache_inval.pl',
       't/008_replslot_single_user.pl',
       't/009_log_temp_files.pl',
+      't/010_mxid_wraparound.pl',
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/010_mxid_wraparound.pl b/src/test/modules/test_misc/t/010_mxid_wraparound.pl
new file mode 100644
index 00000000000..487cb71eacc
--- /dev/null
+++ b/src/test/modules/test_misc/t/010_mxid_wraparound.pl
@@ -0,0 +1,123 @@
+#
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+sub print_controldata_info
+{
+	my $node = shift;
+	my ($stdout, $stderr) = run_command([ 'pg_controldata', $node->data_dir ]);
+
+	foreach (split("\n", $stdout))
+	{
+		if ($_ =~ /^Latest checkpoint's Next\s*(.*)$/mg or
+			$_ =~ /^Latest checkpoint's oldest\s*(.*)$/mg)
+		{
+			print $_."\n";
+		}
+	}
+}
+
+sub create_mxid
+{
+	my $node = shift;
+	my $conn1 = $node->background_psql('postgres');
+	my $conn2 = $node->background_psql('postgres');
+
+	$conn1->query_safe(qq(
+		BEGIN;
+		SELECT * FROM test_table WHERE id = 1 FOR SHARE;
+	));
+	$conn2->query_safe(qq(
+		BEGIN;
+		SELECT * FROM test_table WHERE id = 1 FOR SHARE;
+	));
+
+	$conn1->query_safe(qq(COMMIT;));
+	$conn2->query_safe(qq(COMMIT;));
+
+	$conn1->quit;
+	$conn2->quit;
+}
+
+# 1) Create test cluster
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+$node->start;
+
+$node->safe_psql('postgres',
+qq(
+	CREATE TABLE test_table (id integer NOT NULL PRIMARY KEY, val text);
+	INSERT INTO test_table VALUES (1, 'a');
+));
+
+create_mxid($node);
+
+$node->safe_psql('postgres', qq(UPDATE pg_database SET datallowconn = TRUE WHERE datname = 'template0';));
+$node->stop;
+
+# 2) Advance mxid to UINT32_MAX. We do it in three steps, with vacuums in between, to avoid
+# causing a situation where datminmxid has already wrapped around
+
+# Step 1
+command_ok(
+	[ 'pg_resetwal', '-m', '1492123648,1', $node->data_dir ],
+	'approaching the mxid limit');
+$node->start;
+create_mxid($node);
+$node->command_ok([ 'vacuumdb', '-a', '--freeze' ], 'vacuum all databases');
+$node->stop;
+
+print ">>> pg_controldata: \n";
+print_controldata_info($node);
+
+# Step 2
+command_ok(
+	[ 'pg_resetwal', '-m', '2984247296,1492123648', $node->data_dir ],
+	'approaching the mxid limit');
+$node->start;
+create_mxid($node);
+$node->command_ok([ 'vacuumdb', '-a', '--freeze' ], 'vacuum all databases');
+$node->stop;
+
+# Step 3. This finally gets us to UINT32_MAX.
+command_ok(
+	[ 'pg_resetwal', '-m', '4294967295,2984247296', $node->data_dir ],
+	'approaching the mxid limit');
+
+print ">>> pg_controldata: \n";
+print_controldata_info($node);
+
+# The last step advances nextMulti to value that's not at the beginning of SLRU segment,
+# Postgres expects the segment file to already exit. Create it.
+my $offsets_seg = $node->data_dir . '/pg_multixact/offsets/FFFF';
+open my $fh1, '>', $offsets_seg or BAIL_OUT($!);
+binmode $fh1;
+print $fh1 pack("x[262144]");
+close $fh1;
+
+
+$node->start;
+create_mxid($node);
+$node->command_ok([ 'vacuumdb', '-a', '--freeze' ], 'vacuum all databases');
+is($node->safe_psql('postgres', qq(TABLE test_table;)),
+	'1|a',
+	'check table contents');
+$node->stop;
+
+ok( !$node->log_contains("wraparound protections are disabled"),
+	"check that log doesn't contain 'wraparound protections are disabled'");
+
+ok( !$node->log_contains("cannot truncate up to MultiXact"),
+	"check that log doesn't contain 'cannot truncate up to MultiXact'");
+
+ok( !$node->log_contains("skipping truncation"),
+	"check that log doesn't contain 'skipping truncation'");
+
+done_testing();
-- 
2.47.3

