From 65e508354aa841a918a88ea653efb4b8f69161de Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Mon, 21 Sep 2026 20:48:01 +0000 Subject: [PATCH 2/2] Fix publication table list for a partition pending detach. Commit 479a931155c6 and the previous commit treat a partition whose concurrent detach is committed but not finalized as a standalone table, in relcache.c and pgoutput. pg_get_publication_tables() still went by relispartition alone, so with publish_via_partition_root it did not list such a partition, and treated it as excluded when its former root is in the EXCEPT clause, while pgoutput published its changes under the partition's own name. A subscriber then either failed to apply those changes for lack of the table or skipped them silently, and REFRESH PUBLICATION could not add the table before DETACH PARTITION FINALIZE. Decide on the ancestor list instead, as the other two places do. --- src/backend/catalog/pg_publication.c | 40 ++++++++++++++++++++----- src/test/subscription/t/037_except.pl | 42 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index f11ea8839ca..d9eda70ecf3 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -1077,6 +1077,25 @@ GetAllTablesPublications(void) return result; } +/* + * Returns true if the given partition still has ancestors. + * + * A partition whose concurrent detach has been committed but not finalized + * has relispartition set, but get_partition_ancestors() reports nothing for + * it. Publications treat such a partition as a standalone table, as after + * the detach is finalized, so relispartition alone must not be used to + * decide whether a relation is published via its root. + */ +static bool +partition_has_ancestors(Oid relid) +{ + List *ancestors = get_partition_ancestors(relid); + bool result = (ancestors != NIL); + + list_free(ancestors); + return result; +} + /* * Gets list of all relations published by FOR ALL TABLES/SEQUENCES * publication. @@ -1122,7 +1141,8 @@ GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot) Oid relid = relForm->oid; if (is_publishable_class(relid, relForm) && - !(relForm->relispartition && pubviaroot) && + !(pubviaroot && relForm->relispartition && + partition_has_ancestors(relid)) && !list_member_oid(exceptlist, relid)) result = lappend_oid(result, relid); } @@ -1144,7 +1164,8 @@ GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot) Oid relid = relForm->oid; if (is_publishable_class(relid, relForm) && - !relForm->relispartition && + !(relForm->relispartition && + partition_has_ancestors(relid)) && !list_member_oid(exceptlist, relid)) result = lappend_oid(result, relid); } @@ -1370,7 +1391,6 @@ GetPublicationByName(const char *pubname, bool missing_ok) static bool is_table_publishable_in_publication(Oid relid, Publication *pub) { - bool relispartition; List *ancestors = NIL; /* @@ -1380,9 +1400,13 @@ is_table_publishable_in_publication(Oid relid, Publication *pub) if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE) return false; - relispartition = get_rel_relispartition(relid); - - if (relispartition) + /* + * 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 (get_rel_relispartition(relid)) ancestors = get_partition_ancestors(relid); if (pub->alltables) @@ -1391,7 +1415,7 @@ is_table_publishable_in_publication(Oid relid, Publication *pub) * ALL TABLES with pubviaroot includes only regular tables or top-most * partitioned tables -- never child partitions. */ - if (pub->pubviaroot && relispartition) + if (pub->pubviaroot && ancestors) return false; /* @@ -1428,7 +1452,7 @@ is_table_publishable_in_publication(Oid relid, Publication *pub) * If it's false, the partition is covered by its ancestor's presence in * the publication, it should be included (return true). */ - if (relispartition && + if (ancestors && OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL))) return !pub->pubviaroot; diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl index da5ac89375c..312cdfc9816 100644 --- a/src/test/subscription/t/037_except.pl +++ b/src/test/subscription/t/037_except.pl @@ -373,4 +373,46 @@ $result = $node_subscriber->safe_psql('postgres', is($result, qq(1), 'detach-pending partition is replicated as a standalone table'); +# The catalog agrees with the decoding side about the detach-pending partition +# being a table of its own: it is listed regardless of +# publish_via_partition_root, and its former root in the EXCEPT clause does +# not exclude it. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub_detach_except FOR ALL TABLES EXCEPT (TABLE tab_detach) + WITH (publish_via_partition_root = true))); +$result = $node_publisher->safe_psql('postgres', + "SELECT pubname FROM pg_publication_tables WHERE tablename = 'tab_detach1' ORDER BY pubname" +); +is( $result, qq(tap_pub_detach +tap_pub_detach_except +tap_pub_detach_viaroot), + 'detach-pending partition is listed in pg_publication_tables'); + +# Initial sync and replication via a publish_via_partition_root publication +# use the partition itself. +$node_subscriber->safe_psql( + 'postgres', qq( + DROP SUBSCRIPTION tap_sub; + TRUNCATE tab_detach1; + CREATE TABLE tab_detach (a int PRIMARY KEY); + CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub_detach_viaroot; +)); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$result = $node_subscriber->safe_psql('postgres', + "SELECT * FROM tab_detach1"); +is($result, qq(1), + 'detach-pending partition is synced via a publish_via_partition_root publication' +); + +$node_publisher->safe_psql('postgres', "DELETE FROM tab_detach1"); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM tab_detach1"); +is($result, qq(0), + 'detach-pending partition is replicated via a publish_via_partition_root publication' +); + done_testing(); -- 2.55.0