From 197d69c52b7946a1e17d122fa037db5d5194ed4f Mon Sep 17 00:00:00 2001 From: Nisha Moond Date: Tue, 14 Apr 2026 11:34:07 +0530 Subject: [PATCH v1 3/3] Add EXCEPT support to ALTER PUBLICATION SET TABLES IN SCHEMA Extend AlterPublicationExceptTables() with the AP_SetObjects case, which redefine the publication and replaces the entire EXCEPT list. Syntax: ALTER PUBLICATION pub SET TABLES IN SCHEMA s EXCEPT (s.t1, s.t2); --- src/backend/catalog/pg_publication.c | 2 +- src/backend/commands/publicationcmds.c | 145 +++++++++++++++++++++- src/test/regress/expected/publication.out | 21 ++++ src/test/regress/sql/publication.sql | 12 ++ src/test/subscription/t/037_except.pl | 90 ++++++++++++++ 5 files changed, 267 insertions(+), 3 deletions(-) diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 8e6c4015d86..c03548413e8 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -986,7 +986,7 @@ GetIncludedPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) List * GetExcludedPublicationTables(Oid pubid, PublicationPartOpt pub_partopt) { - Assert(GetPublication(pubid)->alltables); + Assert(GetPublication(pubid)->alltables || is_schema_publication(pubid)); return get_publication_relations(pubid, pub_partopt, true); } diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index fd73511220d..bde99d6bb90 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -1611,9 +1611,11 @@ AlterPublicationExceptTables(AlterPublicationStmt *stmt, Oid pubid = pubform->oid; /* - * Nothing to do if no EXCEPT entries. + * Nothing to do if no EXCEPT entries, except in SET: for that it is + * quite possible that the user has removed all exceptions, in which case + * we need to drop any existing ones. */ - if (!exceptrelations) + if (!exceptrelations && stmt->action != AP_SetObjects) return; /* @@ -1700,6 +1702,106 @@ AlterPublicationExceptTables(AlterPublicationStmt *stmt, PublicationAddTables(pubid, rels, false, stmt); + CloseTableList(rels); + } + else /* AP_SetObjects */ + { + List *oldexceptrelids = NIL; + List *newexceptrelids = NIL; + List *delrelids = NIL; + List *rels; + List *explicitrelids; + List *pubschemaidlist; + ListCell *lc; + + rels = OpenTableList(exceptrelations); + + /* Collect OIDs of the desired new except list. */ + foreach(lc, rels) + { + PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc); + + newexceptrelids = lappend_oid(newexceptrelids, + RelationGetRelid(pri->relation)); + } + + /* + * Build the effective schema list: schemas already in the publication + * plus any being set in this same command. The latter may not yet be + * visible via a catalog scan, so we include them explicitly. + */ + pubschemaidlist = GetPublicationSchemas(pubid); + pubschemaidlist = list_concat_unique_oid(pubschemaidlist, schemaidlist); + explicitrelids = GetIncludedPublicationRelations(pubid, + PUBLICATION_PART_ROOT); + + foreach(lc, rels) + { + PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc); + Oid relid = RelationGetRelid(pri->relation); + Oid relns = RelationGetNamespace(pri->relation); + + if (!list_member_oid(pubschemaidlist, relns)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table \"%s.%s\" in EXCEPT clause does not belong to any schema listed in the publication", + get_namespace_name(relns), + RelationGetRelationName(pri->relation))); + + if (list_member_oid(explicitrelids, relid)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table \"%s.%s\" cannot appear in both the table list and the EXCEPT clause", + get_namespace_name(relns), + RelationGetRelationName(pri->relation))); + } + + /* + * Get the current set of except entries. Only FOR ALL TABLES and + * schema-level publications can have except entries; for any other + * publication type oldexceptrelids stays NIL. + * + * Note: we check is_schema_publication() against the current catalog + * state (before AlterPublicationSchemas has run), so if the caller is + * doing SET TABLE t1 to convert a schema publication into a plain table + * publication, is_schema_publication() still returns true here. That + * is intentional: it lets us discover and clean up any stale except + * entries that belong to the old schema definition. + */ + if (GetPublication(pubid)->alltables || is_schema_publication(pubid)) + oldexceptrelids = GetExcludedPublicationTables(pubid, + PUBLICATION_PART_ROOT); + + /* Build a list of old except entries not present in the new list. */ + foreach(lc, oldexceptrelids) + { + Oid oldrelid = lfirst_oid(lc); + + if (!list_member_oid(newexceptrelids, oldrelid)) + delrelids = lappend_oid(delrelids, oldrelid); + } + + /* Drop old except entries not present in the new list. */ + foreach(lc, delrelids) + { + Oid relid = lfirst_oid(lc); + Oid proid; + ObjectAddress obj; + + proid = GetSysCacheOid2(PUBLICATIONRELMAP, + Anum_pg_publication_rel_oid, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(pubid)); + if (!OidIsValid(proid)) + continue; /* already gone */ + + ObjectAddressSet(obj, PublicationRelRelationId, proid); + performDeletion(&obj, DROP_CASCADE, 0); + } + + /* Add new except entries, skipping any already present. */ + PublicationAddTables(pubid, rels, true, stmt); + CloseTableList(rels); } } @@ -2354,6 +2456,8 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok) foreach(lc, schemas) { Oid schemaid = lfirst_oid(lc); + List *exceptoids; + ListCell *elc; psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP, Anum_pg_publication_namespace_oid, @@ -2370,8 +2474,45 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok) get_namespace_name(schemaid)))); } + /* + * Collect prexcept rows for tables belonging to this schema before + * removing the schema entry. GetExcludedPublicationTables relies on + * is_schema_publication(), which scans pg_publication_namespace; if + * this is the last schema in the publication, performDeletion() below + * would remove that row and make is_schema_publication() return + * false, tripping the assertion. + */ + exceptoids = GetExcludedPublicationTables(pubid, PUBLICATION_PART_ROOT); + ObjectAddressSet(obj, PublicationNamespaceRelationId, psid); performDeletion(&obj, DROP_CASCADE, 0); + + /* + * Drop any prexcept rows for tables belonging to this schema. These + * rows have no pg_depend entry pointing at the + * pg_publication_namespace row, so they are not cascaded by the + * performDeletion() call above and must be cleaned up explicitly. + */ + foreach(elc, exceptoids) + { + Oid relid = lfirst_oid(elc); + Oid proid; + + if (get_rel_namespace(relid) != schemaid) + continue; + + proid = GetSysCacheOid2(PUBLICATIONRELMAP, + Anum_pg_publication_rel_oid, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(pubid)); + if (!OidIsValid(proid)) + continue; /* already gone */ + + ObjectAddressSet(obj, PublicationRelRelationId, proid); + performDeletion(&obj, DROP_CASCADE, 0); + } + + list_free(exceptoids); } } diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index e563553e8eb..f346e77ee57 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -517,6 +517,27 @@ Tables from schemas: Except tables: "pub_test.testpub_tbl_s1" +-- SET: replace the except list (keep same schema, different except table) +ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (pub_test.testpub_tbl_s2); +\dRp+ testpub_alter_except + Publication testpub_alter_except + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | +Tables from schemas: + "pub_test" +Except tables: + "pub_test.testpub_tbl_s2" + +-- error: EXCEPT is not allowed with DROP +ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test EXCEPT (pub_test.testpub_tbl_s2); +ERROR: EXCEPT clause is not supported with DROP in ALTER PUBLICATION +-- error: except table must be schema-qualified +ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (testpub_tbl_s1); +ERROR: table "testpub_tbl_s1" in EXCEPT clause must be schema-qualified +-- error: except table's schema (public) not in the publication's schema list (pub_test) +ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (public.testpub_tbl1); +ERROR: table "public.testpub_tbl1" in EXCEPT clause does not belong to any schema listed in the publication RESET client_min_messages; DROP TABLE pub_test.testpub_tbl_s1, pub_test.testpub_tbl_s2; DROP PUBLICATION testpub_schema_except1, testpub_schema_except2, testpub_alter_except; diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 3436b832eca..6de78800aa5 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -249,6 +249,18 @@ CREATE PUBLICATION testpub_alter_except; ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (pub_test.testpub_tbl_s1); \dRp+ testpub_alter_except +-- SET: replace the except list (keep same schema, different except table) +ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (pub_test.testpub_tbl_s2); +\dRp+ testpub_alter_except + +-- error: EXCEPT is not allowed with DROP +ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test EXCEPT (pub_test.testpub_tbl_s2); + +-- error: except table must be schema-qualified +ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (testpub_tbl_s1); +-- error: except table's schema (public) not in the publication's schema list (pub_test) +ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (public.testpub_tbl1); + RESET client_min_messages; DROP TABLE pub_test.testpub_tbl_s1, pub_test.testpub_tbl_s2; DROP PUBLICATION testpub_schema_except1, testpub_schema_except2, testpub_alter_except; diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl index 9675db18bef..e622bfb3d7f 100644 --- a/src/test/subscription/t/037_except.pl +++ b/src/test/subscription/t/037_except.pl @@ -378,6 +378,66 @@ $result = is($result, qq(0), 'ALTER ... ADD TABLES IN SCHEMA EXCEPT: excluded table not synced'); +# SET: replace the except list; tab_exc is now included and tab_pub is excluded. +$node_publisher->safe_psql('postgres', + "ALTER PUBLICATION sch_pub SET TABLES IN SCHEMA sch1 EXCEPT (sch1.tab_pub)" +); +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION sch_sub REFRESH PUBLICATION"); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'sch_sub'); + +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO sch1.tab_pub VALUES (7); + INSERT INTO sch1.tab_exc VALUES (7); +)); +$node_publisher->wait_for_catchup('sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_exc"); +is($result, qq(1), + 'ALTER ... SET TABLES IN SCHEMA EXCEPT: newly included table is replicated' +); +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_pub"); +is($result, qq(6), + 'ALTER ... SET TABLES IN SCHEMA EXCEPT: now-excluded table is not replicated' +); + +# SET without EXCEPT: clears the except list; both tables are now published. +# tab_pub will be re-synced because REFRESH removed its entry when it was +# excluded. Truncate the subscriber copy beforehand so the re-sync produces +# a predictable count: publisher has 7 rows (6 original + INSERT(7)), so the +# subscriber ends up with 7 after re-sync, then 8 after INSERT(8). +$node_subscriber->safe_psql('postgres', 'TRUNCATE sch1.tab_pub'); +$node_publisher->safe_psql('postgres', + "ALTER PUBLICATION sch_pub SET TABLES IN SCHEMA sch1"); +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION sch_sub REFRESH PUBLICATION"); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'sch_sub'); + +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO sch1.tab_pub VALUES (8); + INSERT INTO sch1.tab_exc VALUES (8); +)); +$node_publisher->wait_for_catchup('sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_pub"); +is($result, qq(8), + 'ALTER ... SET TABLES IN SCHEMA (no EXCEPT): tab_pub replicated after except list cleared' +); +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_exc"); +is($result, qq(2), + 'ALTER ... SET TABLES IN SCHEMA (no EXCEPT): tab_exc replicated after except list cleared' +); + $node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sch_sub'); $node_publisher->safe_psql('postgres', 'DROP PUBLICATION sch_pub'); @@ -445,6 +505,36 @@ $node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub'); $node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1'); $node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2'); +# OK when a table is excluded by a TABLES IN SCHEMA EXCEPT publication, +# but is included by another publication. +$node_publisher->safe_psql('postgres', 'TRUNCATE tab1'); +$node_subscriber->safe_psql('postgres', 'TRUNCATE tab1'); + +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub1 FOR TABLES IN SCHEMA public EXCEPT (public.tab1); + CREATE PUBLICATION tap_pub2 FOR TABLE tab1; + INSERT INTO tab1 VALUES(1); +)); +$node_subscriber->psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2))); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a"); +is( $result, qq(1 +2), + "TABLES IN SCHEMA EXCEPT: table excluded in schema pub but included by another pub is replicated" +); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2'); + $node_publisher->stop('fast'); done_testing(); -- 2.50.1 (Apple Git-155)