From 71152be738be30272341890a93ef0309e358b7e6 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Fri, 14 Aug 2026 14:50:02 +0530 Subject: [PATCH v27 2/4] Add EXCEPT support to ALTER PUBLICATION ADD TABLES IN SCHEMA Extend the EXCEPT clause support to allow tables to be excluded when adding a schema to a publication via ALTER PUBLICATION ... ADD. Syntax: ALTER PUBLICATION pub ADD TABLES IN SCHEMA s EXCEPT (TABLE s.t1); Since pg_dump uses ALTER PUBLICATION ... ADD, support for it is included in this patch. --- src/backend/catalog/pg_publication.c | 19 +- src/backend/commands/publicationcmds.c | 211 +++++++++++++++++----- src/bin/pg_dump/pg_dump.c | 30 ++- src/bin/pg_dump/t/002_pg_dump.pl | 24 +++ src/bin/psql/tab-complete.in.c | 17 ++ src/test/regress/expected/publication.out | 68 ++++++- src/test/regress/sql/publication.sql | 45 ++++- src/test/subscription/t/037_except.pl | 32 ++++ 8 files changed, 390 insertions(+), 56 deletions(-) diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 81e4166cfee..12a360347d2 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -749,15 +749,18 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, * here, as CreatePublication() function invalidates all relations as part * of defining a FOR ALL TABLES publication. * - * For ALTER PUBLICATION, invalidation is needed only when adding an - * EXCEPT table to a publication already marked as ALL TABLES. For - * publications that were originally empty or defined as ALL SEQUENCES and - * are being converted to ALL TABLES, invalidation is skipped here, as - * AlterPublicationAllFlags() function invalidates all relations while - * marking the publication as ALL TABLES publication. + * For ALTER PUBLICATION, invalidation is needed when adding an EXCEPT + * table to either a FOR ALL TABLES publication (pub->alltables is true) + * or a FOR TABLES IN SCHEMA publication (is_schema_publication is true). + * The exception: when a publication is being converted to FOR ALL TABLES + * (pub->alltables is still false at this point), + * AlterPublicationAllFlags() will perform a full invalidation, so we skip + * it here. */ - inval_except_table = (alter_stmt != NULL) && pub->alltables && - (alter_stmt->for_all_tables && pri->except); + inval_except_table = (alter_stmt != NULL) && pri->except && + (pub->alltables + ? alter_stmt->for_all_tables + : is_schema_publication(pubid)); if (!pri->except || inval_except_table) { diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index 77a829bc1bd..30fccf68d1d 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -70,13 +70,25 @@ static void PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, static void PublicationDropTables(Oid pubid, List *rels, bool missing_ok); static void PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists, AlterPublicationStmt *stmt); +static void PublicationAddExceptTables(Oid pubid, List *except_pubtables, + List *explicitrelids, + List *schemaidlist, + AlterPublicationStmt *stmt); static void PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok); +static void AlterPublicationSchemas(AlterPublicationStmt *stmt, + HeapTuple tup, List *schemaidlist, + List *except_pubtables); +static void AlterPublicationSchemaExceptTables(AlterPublicationStmt *stmt, + HeapTuple tup, + List *except_pubtables, + List *schemaidlist); static char defGetGeneratedColsOption(DefElem *def); static void CheckExceptNotInTableList(List *except_rels, List *cross_schema_children, List *explicitrelids); -static void CheckExceptChildNotInSchemaList(List *cross_schema_children, - List *schemaidlist, +static void CheckExceptChildNotInSchemaList(Oid pubid, + List *cross_schema_children, + List *pubschemas, List *except_rels); static void ProcessSchemaExceptTables(Oid schemaid, List *except_tables, ParseState *pstate, List **schemas, @@ -941,31 +953,36 @@ CheckExceptNotInTableList(List *except_rels, List *cross_schema_children, } /* - * Check that an EXCEPT clause does not conflict with a TABLES IN SCHEMA - * clause in the same statement. + * Check that an EXCEPT clause does not conflict with a schema published by the + * publication. * * cross_schema_children contains inheritance children excluded through a * parent in another schema. If such a child is also included through a - * TABLES IN SCHEMA clause in the same statement, report the conflict. + * published schema, report the conflict. pubschemas must therefore cover both + * the schemas that this statement publishes and those the publication already + * publishes; ALTER PUBLICATION ... ADD names only the former. * - * If the child's schema also has an EXCEPT clause specified through TABLES - * IN SCHEMA, there is no conflict, since both clauses exclude the child. + * If the child is excluded anyway, there is no conflict, since both sides + * agree. That is so when an EXCEPT clause of this statement names it, and also + * when the publication already holds an EXCEPT entry for it. * * Conflicts with explicitly listed tables are handled by * CheckExceptNotInTableList(). */ static void -CheckExceptChildNotInSchemaList(List *cross_schema_children, List *schemaidlist, - List *except_rels) +CheckExceptChildNotInSchemaList(Oid pubid, List *cross_schema_children, + List *pubschemas, List *except_rels) { foreach_oid(childrelid, cross_schema_children) { Oid childnsp = get_rel_namespace(childrelid); bool excluded = false; + bool is_except; - if (!list_member_oid(schemaidlist, childnsp)) + if (!list_member_oid(pubschemas, childnsp)) continue; + /* Does an EXCEPT clause of this statement exclude it as well? */ foreach_ptr(PublicationRelInfo, pri, except_rels) { if (RelationGetRelid(pri->relation) == childrelid) @@ -975,6 +992,12 @@ CheckExceptChildNotInSchemaList(List *cross_schema_children, List *schemaidlist, } } + /* Or does the publication already exclude it? */ + if (!excluded && + CheckPublicationRelEntry(pubid, childrelid, &is_except) && + is_except) + excluded = true; + if (excluded) continue; @@ -985,11 +1008,65 @@ CheckExceptChildNotInSchemaList(List *cross_schema_children, List *schemaidlist, get_rel_name(childrelid))), errdetail("It inherits from a table named in the EXCEPT clause of another schema, and its own schema \"%s\" is published in full.", get_namespace_name(childnsp)), - errhint("Use ONLY in the EXCEPT clause to exclude just the parent, or name this table in the EXCEPT clause of schema \"%s\".", + errhint("Use ONLY in the EXCEPT clause to exclude just the parent, or arrange for schema \"%s\" to exclude this table as well.", get_namespace_name(childnsp))); } } +/* + * Add the EXCEPT tables of a statement's TABLES IN SCHEMA clauses to a + * publication, after checking that the statement does not contradict itself. + * + * explicitrelids is the set of relations the statement publishes by name, and + * schemaidlist the schemas it publishes in full; both are needed to detect a + * table that would be published and excluded at the same time. + * + * if_not_exists is false: an EXCEPT entry that is already present, or that + * collides with an ordinary member, is a conflict worth reporting rather than + * something to skip silently. + */ +static void +PublicationAddExceptTables(Oid pubid, List *except_pubtables, + List *explicitrelids, List *schemaidlist, + AlterPublicationStmt *stmt) +{ + List *except_rels; + List *cross_schema_children = NIL; + List *pubschemas; + + if (except_pubtables == NIL) + return; + + except_rels = OpenTableList(except_pubtables, &cross_schema_children); + + /* + * A table is published by this publication if the statement publishes its + * schema, or if the publication already published it. ALTER PUBLICATION + * ... ADD names only the schemas being added, so the catalog has to be + * consulted as well; otherwise a child in a schema that an earlier command + * added would not be recognised as a conflict. + */ + pubschemas = list_concat_unique_oid(list_copy(schemaidlist), + GetPublicationSchemas(pubid)); + + /* + * Validate that a table is not both explicitly included and excluded by + * the schema's EXCEPT clause. + */ + CheckExceptNotInTableList(except_rels, cross_schema_children, + explicitrelids); + + /* + * Validate that an inheritance child is not both excluded by an EXCEPT + * clause and included by its schema. + */ + CheckExceptChildNotInSchemaList(pubid, cross_schema_children, pubschemas, + except_rels); + + PublicationAddTables(pubid, except_rels, false, stmt); + CloseTableList(except_rels); +} + /* * Given a list of tables that are going to be added to a publication, * verify that they fulfill the necessary preconditions, namely: no tables @@ -1228,31 +1305,8 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) LockSchemaList(schemaidlist); PublicationAddSchemas(puboid, schemaidlist, true, NULL); - if (except_pubtables != NIL) - { - List *except_rels; - List *cross_schema_children = NIL; - - except_rels = OpenTableList(except_pubtables, - &cross_schema_children); - - /* - * Validate that a table is not both explicitly included and - * excluded by the schema's EXCEPT clause. - */ - CheckExceptNotInTableList(except_rels, cross_schema_children, - explicitrelids); - - /* - * Validate that an inheritance child is not both excluded by - * an EXCEPT clause and included by its schema. - */ - CheckExceptChildNotInSchemaList(cross_schema_children, - schemaidlist, except_rels); - - PublicationAddTables(puboid, except_rels, false, NULL); - CloseTableList(except_rels); - } + PublicationAddExceptTables(puboid, except_pubtables, + explicitrelids, schemaidlist, NULL); } } @@ -1702,7 +1756,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, */ static void AlterPublicationSchemas(AlterPublicationStmt *stmt, - HeapTuple tup, List *schemaidlist) + HeapTuple tup, List *schemaidlist, + List *except_pubtables) { Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); @@ -1779,6 +1834,78 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt, */ PublicationAddSchemas(pubform->oid, schemaidlist, true, stmt); } + + /* + * Increment the command counter so that is_schema_publication() in + * GetExcludedPublicationTables() can see the just-inserted schema + * rows when AlterPublicationSchemaExceptTables runs next. + */ + if (stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) + CommandCounterIncrement(); + + AlterPublicationSchemaExceptTables(stmt, tup, except_pubtables, schemaidlist); +} + +/* + * Alter the EXCEPT list of a schema-level publication. + * + * Adds, removes, or replaces except-table entries in pg_publication_rel + * (rows with prexcept = true). These entries suppress publication of the + * named tables that would otherwise be covered by a FOR TABLES IN SCHEMA + * clause. + */ +static void +AlterPublicationSchemaExceptTables(AlterPublicationStmt *stmt, + HeapTuple tup, List *except_pubtables, + List *schemaidlist) +{ + Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); + Oid pubid = pubform->oid; + + /* + * Nothing to do if no EXCEPT entries. + */ + if (!except_pubtables) + return; + + /* + * This function handles EXCEPT entries for schema-level publications + * only. For FOR ALL TABLES publications, EXCEPT entries are already + * processed by AlterPublicationTables(). + */ + if (schemaidlist == NIL && !is_schema_publication(pubid)) + return; + + /* + * Dropping a schema from a publication removes all its EXCEPT entries via + * cascade. The concept of "drop all schema tables from the publication + * EXCEPT these ones" is not supported. + */ + if (stmt->action == AP_DropObjects) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("EXCEPT clause is not supported with DROP in ALTER PUBLICATION"))); + + /* + * XXX EXCEPT with SET is not currently implemented. Workaround: DROP and + * re-ADD the schema with the desired EXCEPT list. + */ + if (stmt->action == AP_SetObjects) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("EXCEPT clause is not supported with SET in ALTER PUBLICATION"), + errhint("Drop and re-add the schema with the desired EXCEPT list."))); + + if (stmt->action == AP_AddObjects) + { + List *explicitrelids; + + explicitrelids = GetIncludedPublicationRelations(pubid, + PUBLICATION_PART_ROOT); + + PublicationAddExceptTables(pubid, except_pubtables, explicitrelids, + schemaidlist, stmt); + } } /* @@ -1960,12 +2087,12 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) else { List *relations = NIL; - List *exceptrelations = NIL; + List *except_pubtables = NIL; List *schemaidlist = NIL; Oid pubid = pubform->oid; ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, - &exceptrelations, &schemaidlist); + &except_pubtables, &schemaidlist); CheckAlterPublication(stmt, tup, relations, schemaidlist); @@ -1988,10 +2115,12 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) errmsg("publication \"%s\" does not exist", stmt->pubname)); - relations = list_concat(relations, exceptrelations); + if (stmt->for_all_tables) + relations = list_concat(relations, except_pubtables); + AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext, schemaidlist != NIL); - AlterPublicationSchemas(stmt, tup, schemaidlist); + AlterPublicationSchemas(stmt, tup, schemaidlist, except_pubtables); AlterPublicationAllFlags(stmt, rel, tup); } diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index feed88f9854..ddbd5e40285 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -4973,6 +4973,7 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo) PublicationInfo *pubinfo = pubsinfo->publication; PQExpBuffer query; char *tag; + bool has_except = false; /* Do nothing if not dumping schema */ if (!dopt->dumpSchema) @@ -4983,7 +4984,34 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo) query = createPQExpBuffer(); appendPQExpBuffer(query, "ALTER PUBLICATION %s ", fmtId(pubinfo->dobj.name)); - appendPQExpBuffer(query, "ADD TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name)); + appendPQExpBuffer(query, "ADD TABLES IN SCHEMA %s", fmtId(schemainfo->dobj.name)); + + /* + * Append EXCEPT clause for any tables that belong to this schema + * and are excluded from the publication. + */ + for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell; cell = cell->next) + { + TableInfo *tbinfo = (TableInfo *) cell->ptr; + + if (strcmp(tbinfo->dobj.namespace->dobj.name, schemainfo->dobj.name) == 0) + { + if (!has_except) + { + appendPQExpBufferStr(query, " EXCEPT ("); + has_except = true; + } + else + appendPQExpBufferStr(query, ", "); + + appendPQExpBuffer(query, "TABLE ONLY %s", fmtId(tbinfo->dobj.name)); + } + } + + if (has_except) + appendPQExpBufferStr(query, ")"); + + appendPQExpBufferStr(query, ";\n"); /* * There is no point in creating drop query as the drop is done by schema diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 9258948b583..d18b3b0d7b9 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3283,6 +3283,30 @@ my %tests = ( like => { %full_runs, section_post_data => 1, }, }, + 'CREATE PUBLICATION pub11' => { + create_order => 50, + create_sql => + 'CREATE PUBLICATION pub11 FOR TABLES IN SCHEMA dump_test EXCEPT (TABLE test_table);', + regexp => qr/^ + \QCREATE PUBLICATION pub11 WITH (publish = 'insert, update, delete, truncate');\E + .*? + \QALTER PUBLICATION pub11 ADD TABLES IN SCHEMA dump_test EXCEPT (TABLE ONLY test_table);\E + /xms, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub12' => { + create_order => 50, + create_sql => + 'CREATE PUBLICATION pub12 FOR TABLES IN SCHEMA dump_test EXCEPT (TABLE test_table, dump_test.test_second_table);', + regexp => qr/^ + \QCREATE PUBLICATION pub12 WITH (publish = 'insert, update, delete, truncate');\E + .*? + \QALTER PUBLICATION pub12 ADD TABLES IN SCHEMA dump_test EXCEPT (TABLE ONLY test_table, TABLE ONLY test_second_table);\E + /xms, + like => { %full_runs, section_post_data => 1, }, + }, + 'CREATE SUBSCRIPTION sub1' => { create_order => 50, create_sql => 'CREATE SUBSCRIPTION sub1 diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index f069e2654cc..6d857865d6b 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2359,6 +2359,23 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas " AND nspname NOT LIKE E'pg\\\\_%%'", "CURRENT_SCHEMA"); + /* After a single schema name in ADD context, offer EXCEPT ( TABLE */ + else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", MatchAny) && + !ends_with(prev_wd, ',')) + COMPLETE_WITH("EXCEPT ( TABLE"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT")) + COMPLETE_WITH("( TABLE"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(")) + COMPLETE_WITH("TABLE"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", "CURRENT_SCHEMA", "EXCEPT", "(", "TABLE")) + COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_tables_in_current_schema); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE")) + { + set_completion_reference(prev4_wd); + COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_tables_in_schema); + } + else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE", MatchAnyN) && !ends_with(prev_wd, ',')) + COMPLETE_WITH(")"); /* ALTER PUBLICATION SET ( */ else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "SET", "(")) COMPLETE_WITH("publish", "publish_generated_columns", "publish_via_partition_root"); diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 6befa549ae6..5ef61587a15 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -671,14 +671,14 @@ DROP PUBLICATION testpub_except_alter1; CREATE PUBLICATION testpub_except_alter2 FOR TABLE pub_test.testpub_tbl_s1; ALTER PUBLICATION testpub_except_alter2 SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +ERROR: EXCEPT clause is not supported with SET in ALTER PUBLICATION +HINT: Drop and re-add the schema with the desired EXCEPT list. \dRp+ testpub_except_alter2 Publication testpub_except_alter2 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: +Tables: "pub_test.testpub_tbl_s1" DROP PUBLICATION testpub_except_alter2; @@ -709,7 +709,7 @@ CREATE PUBLICATION testpub_inh TABLES IN SCHEMA public; ERROR: table "public.testpub_inh_child" cannot be both published and excluded DETAIL: It inherits from a table named in the EXCEPT clause of another schema, and its own schema "public" is published in full. -HINT: Use ONLY in the EXCEPT clause to exclude just the parent, or name this table in the EXCEPT clause of schema "public". +HINT: Use ONLY in the EXCEPT clause to exclude just the parent, or arrange for schema "public" to exclude this table as well. -- fail: the child is also explicitly included by the same statement CREATE PUBLICATION testpub_inh FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent), @@ -756,12 +756,70 @@ Except tables: DROP PUBLICATION testpub_inh; DROP TABLE testpub_inh_child, pub_test.testpub_inh_sibling, pub_test.testpub_inh_parent; +--------------------------------------------- +-- EXCEPT tests for ALTER PUBLICATION +--------------------------------------------- +CREATE PUBLICATION testpub_alter_except; +-- fail: non-existing table in EXCEPT clause +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table); +ERROR: relation "pub_test.nonexistent_table" does not exist +-- fail: EXCEPT table belongs to a different schema +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE public.testpub_tbl1); +ERROR: table "public.testpub_tbl1" in EXCEPT clause does not belong to schema "pub_test" +LINE 1: ...xcept ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE public.tes... + ^ +-- fail: TABLE keyword is required for the first entry in EXCEPT clause +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (testpub_nopk); +ERROR: syntax error at or near "testpub_nopk" +LINE 1: ...lter_except ADD TABLES IN SCHEMA pub_test EXCEPT (testpub_no... + ^ +-- fail: exact same table given as both explicitly published and excluded +-- in a single ALTER ... ADD +ALTER PUBLICATION testpub_alter_except + ADD TABLE pub_test.testpub_tbl_s1, + TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +ERROR: table "pub_test.testpub_tbl_s1" cannot be both published and excluded +-- fail: add explicit partition and EXCEPT ancestor, given in a +-- single ALTER ... ADD statement +ALTER PUBLICATION testpub_alter_except + ADD TABLE pub_test.testpub_part_s, + TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); +ERROR: partition "pub_test.testpub_part_s" cannot be both published and excluded +DETAIL: Partition root "pub_test.testpub_parted_s" is named in the publication's EXCEPT clause for schema "pub_test". +-- fail: partition already explicitly published, then its root is EXCEPT-ed +-- via a later, separate ALTER ... ADD +ALTER PUBLICATION testpub_alter_except ADD TABLE pub_test.testpub_part_s; +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); +ERROR: partition "pub_test.testpub_part_s" cannot be both published and excluded +DETAIL: Partition root "pub_test.testpub_parted_s" is named in the publication's EXCEPT clause for schema "pub_test". +ALTER PUBLICATION testpub_alter_except DROP TABLE pub_test.testpub_part_s; +-- fail: root already explicitly published, then its partition is rejected +-- from EXCEPT via a later, separate ALTER ... ADD (partitions can never +-- appear in EXCEPT, independent of the root's state) +ALTER PUBLICATION testpub_alter_except ADD TABLE pub_test.testpub_parted_s; +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_part_s); +ERROR: cannot specify relation "pub_test.testpub_part_s" in the publication EXCEPT clause +DETAIL: This operation is not supported for individual partitions. +ALTER PUBLICATION testpub_alter_except DROP TABLE pub_test.testpub_parted_s; +-- ADD: qualified and unqualified names; unqualified is implicitly qualified with the schema +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1, 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_s1" + "pub_test.testpub_tbl_s2" + -- Cleanup RESET client_min_messages; DROP TABLE pub_test.testpub_tbl_s1, pub_test.testpub_tbl_s2; DROP TABLE pub_test.testpub_parted_s CASCADE; DROP TABLE testpub_nopk, testpub_tbl_s1; -DROP PUBLICATION testpub_schema_except1, testpub_schema_except2, testpub_schema_except_multi; +DROP PUBLICATION testpub_schema_except1, testpub_schema_except2, testpub_schema_except_multi, testpub_alter_except; --------------------------------------------- -- Tests for publications with SEQUENCES --------------------------------------------- diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index f77dba51349..005d40c5c82 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -386,12 +386,55 @@ DROP PUBLICATION testpub_inh; DROP TABLE testpub_inh_child, pub_test.testpub_inh_sibling, pub_test.testpub_inh_parent; +--------------------------------------------- +-- EXCEPT tests for ALTER PUBLICATION +--------------------------------------------- +CREATE PUBLICATION testpub_alter_except; + +-- fail: non-existing table in EXCEPT clause +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table); + +-- fail: EXCEPT table belongs to a different schema +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE public.testpub_tbl1); + +-- fail: TABLE keyword is required for the first entry in EXCEPT clause +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (testpub_nopk); + +-- fail: exact same table given as both explicitly published and excluded +-- in a single ALTER ... ADD +ALTER PUBLICATION testpub_alter_except + ADD TABLE pub_test.testpub_tbl_s1, + TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); + +-- fail: add explicit partition and EXCEPT ancestor, given in a +-- single ALTER ... ADD statement +ALTER PUBLICATION testpub_alter_except + ADD TABLE pub_test.testpub_part_s, + TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); + +-- fail: partition already explicitly published, then its root is EXCEPT-ed +-- via a later, separate ALTER ... ADD +ALTER PUBLICATION testpub_alter_except ADD TABLE pub_test.testpub_part_s; +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); +ALTER PUBLICATION testpub_alter_except DROP TABLE pub_test.testpub_part_s; + +-- fail: root already explicitly published, then its partition is rejected +-- from EXCEPT via a later, separate ALTER ... ADD (partitions can never +-- appear in EXCEPT, independent of the root's state) +ALTER PUBLICATION testpub_alter_except ADD TABLE pub_test.testpub_parted_s; +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_part_s); +ALTER PUBLICATION testpub_alter_except DROP TABLE pub_test.testpub_parted_s; + +-- ADD: qualified and unqualified names; unqualified is implicitly qualified with the schema +ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1, testpub_tbl_s2); +\dRp+ testpub_alter_except + -- Cleanup RESET client_min_messages; DROP TABLE pub_test.testpub_tbl_s1, pub_test.testpub_tbl_s2; DROP TABLE pub_test.testpub_parted_s CASCADE; DROP TABLE testpub_nopk, testpub_tbl_s1; -DROP PUBLICATION testpub_schema_except1, testpub_schema_except2, testpub_schema_except_multi; +DROP PUBLICATION testpub_schema_except1, testpub_schema_except2, testpub_schema_except_multi, testpub_alter_except; --------------------------------------------- -- Tests for publications with SEQUENCES diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl index 7e11061a594..ed931d0d970 100644 --- a/src/test/subscription/t/037_except.pl +++ b/src/test/subscription/t/037_except.pl @@ -347,6 +347,38 @@ is($result, qq(5), $node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sch_sub'); $node_publisher->safe_psql('postgres', 'DROP PUBLICATION sch_pub'); +# ============================================ +# ALTER PUBLICATION EXCEPT for TABLES IN SCHEMA +# ============================================ + +# Truncate subscriber tables to remove data accumulated from previous tests. +$node_subscriber->safe_psql('postgres', + 'TRUNCATE sch1.tab_published, sch1.tab_excluded, sch1.parent, sch1.child'); + +# ADD: add a schema with an excepted table; verify the except entry takes effect. +$node_publisher->safe_psql('postgres', "CREATE PUBLICATION sch_pub"); +$node_publisher->safe_psql('postgres', + "ALTER PUBLICATION sch_pub ADD TABLES IN SCHEMA sch1 EXCEPT (TABLE sch1.tab_excluded)" +); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION sch_sub CONNECTION '$publisher_connstr' PUBLICATION sch_pub" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_published"); +is($result, qq(6), + 'ALTER ... ADD TABLES IN SCHEMA EXCEPT: included table synced'); +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_excluded"); +is($result, qq(0), + 'ALTER ... ADD TABLES IN SCHEMA EXCEPT: excluded table not synced'); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sch_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION sch_pub'); + # Cleanup schema tables before the multi-publication section. $node_publisher->safe_psql('postgres', 'DROP SCHEMA sch1 CASCADE'); $node_subscriber->safe_psql('postgres', 'DROP SCHEMA sch1 CASCADE'); -- 2.55.0