From b5fac79d2f1499e6ca51cb3856743448a37fd50e Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Mon, 21 Sep 2026 20:30:43 +0000 Subject: [PATCH] Fix crash in pgoutput when decoding a partition pending detach. Commit 479a931155c6 taught RelationBuildPublicationDesc() that a partition whose concurrent detach has been committed but not finalized still has relispartition set while get_partition_ancestors() already reports nothing. get_rel_sync_entry() in pgoutput makes the same assumption for FOR ALL TABLES publications: it asks for the last element of the ancestor list to evaluate the EXCEPT clause at the top-most ancestor, which is an assertion failure on an empty list, or a NULL pointer dereference without assertions. Decoding any change of such a partition through a FOR ALL TABLES publication therefore crashed the walsender, and the same happened in a backend using the SQL decoding functions. Treat such a partition as a standalone table, as relcache.c already does: publish it under its own OID and evaluate the EXCEPT clause on the partition itself. Oversight in fd366065e06a, which added the exclusion. --- src/backend/replication/pgoutput/pgoutput.c | 11 ++- src/test/subscription/t/037_except.pl | 86 +++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c index b481d5bb388..55d9fa4af02 100644 --- a/src/backend/replication/pgoutput/pgoutput.c +++ b/src/backend/replication/pgoutput/pgoutput.c @@ -2211,10 +2211,19 @@ get_rel_sync_entry(PGOutputData *data, Relation relation) if (pub->alltables) { List *exceptpubids = NIL; + List *ancestors = NIL; + /* + * A partition whose concurrent detach has been committed but + * not finalized reports no ancestors, even though + * relispartition is still set. Treat such a partition as a + * standalone table, as after the detach is finalized. + */ if (am_partition) + ancestors = get_partition_ancestors(relid); + + if (ancestors) { - List *ancestors = get_partition_ancestors(relid); Oid last_ancestor_relid = llast_oid(ancestors); /* diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl index dd79fbb46e1..da5ac89375c 100644 --- a/src/test/subscription/t/037_except.pl +++ b/src/test/subscription/t/037_except.pl @@ -287,4 +287,90 @@ is( $result, qq(1 "check replication of a table in the EXCEPT clause of one publication but included by another" ); +# ============================================ +# Partition whose concurrent detach has not been finalized +# ============================================ +# ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY leaves the partition +# detach-pending when it is interrupted while waiting for lockers. In that +# state relispartition is still set but the partition has no ancestors, and +# decoding its changes for a FOR ALL TABLES publication must not look for a +# top-most ancestor to evaluate the EXCEPT clause on. +$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub"); +$node_publisher->safe_psql( + 'postgres', qq( + DROP PUBLICATION tap_pub1, tap_pub2; + CREATE TABLE tab_detach (a int PRIMARY KEY) PARTITION BY LIST (a); + CREATE TABLE tab_detach1 PARTITION OF tab_detach FOR VALUES IN (1); + INSERT INTO tab_detach VALUES (1); +)); +$node_subscriber->safe_psql('postgres', + "CREATE TABLE tab_detach1 (a int PRIMARY KEY)"); + +# Hold a lock on the partition so the concurrent detach blocks after its +# first transaction has committed, then cancel it while it waits. +my $lock_session = $node_publisher->background_psql('postgres'); +$lock_session->query_safe("BEGIN; SELECT * FROM tab_detach;"); + +my $detach_session = + $node_publisher->background_psql('postgres', on_error_stop => 0); +my $detach_pid = $detach_session->query('SELECT pg_backend_pid()'); +$detach_session->query_until(qr//, + "ALTER TABLE tab_detach DETACH PARTITION tab_detach1 CONCURRENTLY;\n"); + +$node_publisher->poll_query_until('postgres', + "SELECT wait_event_type = 'Lock' FROM pg_stat_activity WHERE pid = $detach_pid" +) or die "timed out waiting for the concurrent detach to block"; + +$node_publisher->safe_psql('postgres', + "SELECT pg_cancel_backend($detach_pid)"); +ok( pump_until( + $detach_session->{run}, $detach_session->{timeout}, + \$detach_session->{stderr}, + qr/canceling statement due to user request/), + 'concurrent detach canceled'); +$detach_session->quit; +$lock_session->query_safe("COMMIT"); +$lock_session->quit; + +$result = $node_publisher->safe_psql( + 'postgres', qq( + SELECT c.relispartition, i.inhdetachpending + FROM pg_class c JOIN pg_inherits i ON i.inhrelid = c.oid + WHERE c.oid = 'tab_detach1'::regclass)); +is($result, qq(t|t), 'partition is left detach-pending'); + +# Decode an update of the detach-pending partition for FOR ALL TABLES +# publications, with and without publish_via_partition_root. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub_detach FOR ALL TABLES; + CREATE PUBLICATION tap_pub_detach_viaroot FOR ALL TABLES + WITH (publish_via_partition_root = true); + SELECT pg_replication_slot_advance('test_slot', pg_current_wal_lsn()); + UPDATE tab_detach1 SET a = 1; +)); + +foreach my $pub (qw(tap_pub_detach tap_pub_detach_viaroot)) +{ + $result = $node_publisher->safe_psql('postgres', + "SELECT count(*) > 0 FROM pg_logical_slot_peek_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', '$pub')" + ); + is($result, qq(t), + "changes of a detach-pending partition are decoded for $pub"); +} + +# The detach-pending partition is published like a standalone table. +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub_detach" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$node_publisher->safe_psql('postgres', "UPDATE tab_detach1 SET a = 1"); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = $node_subscriber->safe_psql('postgres', + "SELECT * FROM tab_detach1"); +is($result, qq(1), + 'detach-pending partition is replicated as a standalone table'); + done_testing(); -- 2.55.0