From dd349ad06196224b9dc5fb4fc94c14b7aecc7aad Mon Sep 17 00:00:00 2001 From: Satya Narlapuram 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