From dd349ad06196224b9dc5fb4fc94c14b7aecc7aad Mon Sep 17 00:00:00 2001
From: Satya Narlapuram <satyanarlapuram@gmail.com>
Date: Tue, 26 May 2026 22:45:56 +0000
Subject: [PATCH v1 2/2] Add TAP test for check_pub_rdt bypass fix

Adds a regression test that verifies ALTER SUBSCRIPTION SET
(retain_dead_tuples = true, origin = 'none') is correctly rejected
when the publisher is a standby (in recovery).

The test creates a primary, a streaming standby, and a subscriber.
It verifies:
- retain_dead_tuples alone is rejected against a standby
- retain_dead_tuples + origin='none' is also rejected (the bug case)
- retain_dead_tuples + origin='any' is rejected against standby
- The same combined ALTER succeeds against a real primary
---
 src/test/subscription/meson.build             |   1 +
 .../t/101_check_pub_rdt_bypass.pl             | 147 ++++++++++++++++++
 2 files changed, 148 insertions(+)
 create mode 100644 src/test/subscription/t/101_check_pub_rdt_bypass.pl

diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e71e95c6297..a2c7369d9ab 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -49,6 +49,7 @@ tests += {
       't/037_except.pl',
       't/038_walsnd_shutdown_timeout.pl',
       't/100_bugs.pl',
+      't/101_check_pub_rdt_bypass.pl',
     ],
   },
 }
diff --git a/src/test/subscription/t/101_check_pub_rdt_bypass.pl b/src/test/subscription/t/101_check_pub_rdt_bypass.pl
new file mode 100644
index 00000000000..7d085091bd9
--- /dev/null
+++ b/src/test/subscription/t/101_check_pub_rdt_bypass.pl
@@ -0,0 +1,147 @@
+# Copyright (c) 2025-2026, PostgreSQL Global Development Group
+
+# Test that ALTER SUBSCRIPTION SET (retain_dead_tuples = true, origin = 'none')
+# does not bypass the publisher compatibility check for retain_dead_tuples.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+###############################
+# Setup: publisher (primary) + standby of publisher + subscriber
+###############################
+
+# Create the publisher (primary)
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create a standby of the publisher
+$node_publisher->backup('publisher_backup');
+my $node_standby = PostgreSQL::Test::Cluster->new('standby');
+$node_standby->init_from_backup($node_publisher, 'publisher_backup',
+	has_streaming => 1);
+$node_standby->start;
+
+# Confirm standby is in recovery
+my $is_standby = $node_standby->safe_psql('postgres',
+	"SELECT pg_is_in_recovery()");
+is($is_standby, 't', 'standby is in recovery');
+
+# Create a subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Create table and publication on publisher
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE test_rdt (id int PRIMARY KEY)");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION test_pub FOR TABLE test_rdt");
+
+# Create the same table on subscriber
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE test_rdt (id int PRIMARY KEY)");
+
+# Create subscription pointing to the STANDBY as publisher,
+# with connect=false so it doesn't fail during creation
+my $standby_connstr = $node_standby->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION test_sub
+	 CONNECTION '$standby_connstr'
+	 PUBLICATION test_pub
+	 WITH (connect = false, enabled = false, retain_dead_tuples = false)");
+
+###############################
+# Test 1: retain_dead_tuples alone is correctly rejected
+###############################
+
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+	"ALTER SUBSCRIPTION test_sub SET (retain_dead_tuples = true)");
+isnt($ret, 0, 'retain_dead_tuples alone against standby fails');
+like($stderr,
+	qr/cannot enable retain_dead_tuples if the publisher is in recovery/,
+	'error message mentions publisher in recovery');
+
+# Verify retain_dead_tuples is still false
+my $rdt = $node_subscriber->safe_psql('postgres',
+	"SELECT subretaindeadtuples FROM pg_subscription WHERE subname = 'test_sub'");
+is($rdt, 'f',
+	'retain_dead_tuples remains false after failed ALTER');
+
+###############################
+# Test 2: retain_dead_tuples + origin='none' is ALSO correctly rejected
+# (This was the bug: previously this succeeded by bypassing the check)
+###############################
+
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+	"ALTER SUBSCRIPTION test_sub SET (retain_dead_tuples = true, origin = 'none')");
+isnt($ret, 0,
+	'retain_dead_tuples + origin=none against standby fails (bypass fixed)');
+like($stderr,
+	qr/cannot enable retain_dead_tuples if the publisher is in recovery/,
+	'error message for combined ALTER mentions publisher in recovery');
+
+# Verify retain_dead_tuples is still false
+$rdt = $node_subscriber->safe_psql('postgres',
+	"SELECT subretaindeadtuples FROM pg_subscription WHERE subname = 'test_sub'");
+is($rdt, 'f',
+	'retain_dead_tuples remains false after combined ALTER against standby');
+
+###############################
+# Test 3: retain_dead_tuples + origin='any' is also rejected against standby
+###############################
+
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+	"ALTER SUBSCRIPTION test_sub SET (retain_dead_tuples = true, origin = 'any')");
+isnt($ret, 0,
+	'retain_dead_tuples + origin=any against standby also fails');
+like($stderr,
+	qr/cannot enable retain_dead_tuples if the publisher is in recovery/,
+	'error for origin=any also mentions publisher in recovery');
+
+###############################
+# Test 4: same ALTER against the primary publisher should succeed
+###############################
+
+# Drop old subscription and create one pointing to the primary
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION test_sub SET (slot_name = NONE)");
+$node_subscriber->safe_psql('postgres',
+	"DROP SUBSCRIPTION test_sub");
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION test_sub
+	 CONNECTION '$publisher_connstr'
+	 PUBLICATION test_pub
+	 WITH (enabled = false, retain_dead_tuples = false, origin = 'any')");
+
+# Wait for initial sync to not interfere
+$node_subscriber->psql('postgres',
+	"ALTER SUBSCRIPTION test_sub DISABLE");
+$node_subscriber->poll_query_until('postgres',
+	"SELECT count(*) = 0 FROM pg_stat_activity
+	 WHERE backend_type = 'logical replication apply worker'
+	 AND query LIKE '%test_sub%'");
+
+# retain_dead_tuples + origin='none' should succeed against a primary
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+	"ALTER SUBSCRIPTION test_sub SET (retain_dead_tuples = true, origin = 'none')");
+is($ret, 0,
+	'retain_dead_tuples + origin=none succeeds against primary publisher');
+
+$rdt = $node_subscriber->safe_psql('postgres',
+	"SELECT subretaindeadtuples FROM pg_subscription WHERE subname = 'test_sub'");
+is($rdt, 't',
+	'retain_dead_tuples is true after successful ALTER against primary');
+
+my $origin = $node_subscriber->safe_psql('postgres',
+	"SELECT suborigin FROM pg_subscription WHERE subname = 'test_sub'");
+is($origin, 'none',
+	'origin is none after successful ALTER against primary');
+
+done_testing();
-- 
2.43.0

