commit 26ac659 (HEAD -> subscription-conflictlog-reassign-defect) Author: Noah Misch AuthorDate: Fri Jul 10 17:37:45 2026 +0000 Commit: Noah Misch CommitDate: Fri Jul 10 17:37:45 2026 +0000 Add test for cross-database REASSIGN OWNED of a conflict-log subscription pg_subscription is a shared catalog, so REASSIGN OWNED processes a subscription's ownership from every database (shdepReassignOwned). AlterSubscriptionOwner_internal() then calls ATExecChangeOwner() on the subscription's conflict log table (pg_subscription.subconflictlogrelid), which is a per-database relation. Run from any other database, that OID is not valid, so relation_open() raises "could not open relation with OID N". Worse, if that OID happens to be reused by an unrelated relation in the current database, ATExecChangeOwner() silently changes that relation's owner instead -- a latent silent-corruption hazard. This breaks the documented multi-database role-removal workflow (REASSIGN OWNED / DROP OWNED in each database, then DROP ROLE). No foreign server is involved; a plain CONNECTION subscription with conflict_log_destination = 'table' is enough to reach it. It is the same bug class as the subserver case, via a different per-database OID stored in the shared pg_subscription catalog. The new test asserts the current, buggy behavior so it serves as an executable reproduction; comments note the expected correct behavior for when the defect is fixed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TePr48d48d89GukfGsNw9b --- src/test/subscription/t/100_bugs.pl | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 075c52f..05a7f99 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -652,4 +652,79 @@ $node_publisher->safe_psql('postgres', "DROP TABLE tab_upsert"); $node_publisher->stop('fast'); +# REASSIGN OWNED BY of a subscription that has a conflict log table, run from +# a database other than the subscription's own, fails with an internal error. +# +# pg_subscription is a shared catalog, so REASSIGN OWNED (via +# shdepReassignOwned) processes a subscription from every database. +# AlterSubscriptionOwner_internal() then calls ATExecChangeOwner() on the +# subscription's conflict log table (pg_subscription.subconflictlogrelid), +# which is a per-database relation. From any other database that OID is not +# valid, so relation_open() raises "could not open relation with OID N" (and, +# worse, would silently change the owner of an unrelated relation that happens +# to reuse the OID). This breaks the documented multi-database role-removal +# workflow (REASSIGN OWNED / DROP OWNED in each database, then DROP ROLE). +# No foreign server is involved: a plain CONNECTION subscription with +# conflict_log_destination = 'table' is enough to reach this. +# +# This asserts the current, buggy behavior. CORRECT BEHAVIOR: the +# cross-database REASSIGN OWNED should reassign the subscription without an +# internal error (as the in-database case below already does); when fixed, +# the isnt()/like() checks must be flipped to the expected behavior. +{ + my $node = PostgreSQL::Test::Cluster->new('conflict_log_reassign'); + $node->init; + $node->start; + + $node->safe_psql('postgres', 'CREATE ROLE regress_clog_owner'); + $node->safe_psql('postgres', 'CREATE ROLE regress_clog_newowner'); + $node->safe_psql('postgres', 'CREATE DATABASE regress_clog_db'); + + # A plain CONNECTION subscription (no foreign server) that logs conflicts + # to a table. connect => false means no publisher is needed. The + # conflict log table is created in regress_clog_db; hand the subscription + # (and hence the table) to a droppable non-superuser role. + $node->safe_psql( + 'regress_clog_db', q{ + CREATE SUBSCRIPTION regress_clog_sub + CONNECTION 'dbname=regress_nonexistent' PUBLICATION pub + WITH (connect = false, conflict_log_destination = 'table'); + ALTER SUBSCRIPTION regress_clog_sub OWNER TO regress_clog_owner; + }); + + # The subscription uses no server, but does have a conflict log table (a + # per-database relation), which is the ingredient that triggers the bug. + is( $node->safe_psql( + 'regress_clog_db', + q{SELECT subserver = 0 AND subconflictlogrelid <> 0 + FROM pg_subscription WHERE subname = 'regress_clog_sub'}), + 't', + 'conflict-log subscription has no server but a conflict log table'); + + # BUG: from another database ("postgres"), REASSIGN OWNED aborts with an + # internal error because the conflict log table's OID is not valid here. + my ($ret, $stdout, $stderr) = $node->psql('postgres', + 'REASSIGN OWNED BY regress_clog_owner TO regress_clog_newowner'); + isnt($ret, 0, + 'cross-database REASSIGN OWNED of a conflict-log subscription fails'); + like( + $stderr, + qr/could not open relation with OID \d+/, + 'cross-database REASSIGN OWNED raises an internal relation-open error'); + + # Contrast: from the subscription's own database the same command + # succeeds, reassigning both the subscription and its conflict log table. + # The operation is legitimate; only the cross-database path is broken. + $node->safe_psql('regress_clog_db', + 'REASSIGN OWNED BY regress_clog_owner TO regress_clog_newowner'); + is( $node->safe_psql( + 'regress_clog_db', + q{SELECT subowner::regrole::text FROM pg_subscription + WHERE subname = 'regress_clog_sub'}), + 'regress_clog_newowner', + 'in-database REASSIGN OWNED reassigns the conflict-log subscription'); + + $node->stop; +} + done_testing();