commit 26ac659 (HEAD -> subscription-conflictlog-reassign-defect)
Author:     Noah Misch <nm@cqla.c.codeql-202606a.internal>
AuthorDate: Fri Jul 10 17:37:45 2026 +0000
Commit:     Noah Misch <nm@cqla.c.codeql-202606a.internal>
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 <noreply@anthropic.com>
    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();
