From fd2804a83105c4d5278e70fe3515b712b724d01e Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Mon, 10 Aug 2026 21:22:15 +0530 Subject: [PATCH v27 1/4] Support EXCEPT clause for schema-level publications Extend table exclusion support in publications to allow specific tables to be excluded from schema-level publications using an EXCEPT clause in CREATE PUBLICATION. Supported syntax: CREATE PUBLICATION pub FOR TABLES IN SCHEMA s EXCEPT (TABLE t1,...); This reuses the existing opt_pub_except_clause grammar and stores excluded tables as pg_publication_rel entries with prexcept = true, requiring no catalog changes. NOTES - Each EXCEPT clause applies only to its own schema; cross-schema references are rejected. - A schema may appear only once with an EXCEPT clause. - A table cannot be both explicitly published and excluded in the same statement. - Only partition roots may be excluded. Excluding a root excludes the entire partition tree, including partitions in other published schemas. Individual partitions cannot be added while their root is excluded, and ATTACH PARTITION is rejected if the attached table is excluded. - ALTER TABLE ... SET SCHEMA removes schema-scoped exclusions, matching existing schema-publication behavior. - psql describe output and tab completion are updated to support the new syntax. --- src/backend/catalog/pg_publication.c | 274 +++++++++++--- src/backend/commands/publicationcmds.c | 385 +++++++++++++++++++- src/backend/commands/tablecmds.c | 24 +- src/backend/parser/gram.y | 27 +- src/backend/replication/pgoutput/pgoutput.c | 37 +- src/backend/utils/cache/relcache.c | 26 +- src/bin/psql/describe.c | 44 ++- src/bin/psql/tab-complete.in.c | 35 +- src/include/catalog/pg_publication.h | 3 + src/include/commands/publicationcmds.h | 2 + src/include/nodes/parsenodes.h | 5 + src/test/regress/expected/publication.out | 384 ++++++++++++++++++- src/test/regress/sql/publication.sql | 236 +++++++++++- src/test/subscription/t/037_except.pl | 261 ++++++++++++- 14 files changed, 1647 insertions(+), 96 deletions(-) diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 12af7d15536..81e4166cfee 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -54,7 +54,7 @@ typedef struct * error if not. */ static void -check_publication_add_relation(PublicationRelInfo *pri) +check_publication_add_relation(PublicationRelInfo *pri, Oid pubid) { Relation targetrel = pri->relation; const char *relname; @@ -71,12 +71,51 @@ check_publication_add_relation(PublicationRelInfo *pri) errormsg = gettext_noop("cannot add relation \"%s\" to publication"); } - /* If in EXCEPT clause, must be root partitioned table */ - if (pri->except && targetrel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg(errormsg, relname), - errdetail("This operation is not supported for individual partitions."))); + /* + * A partition cannot be added to a publication if its partition root is + * excluded by the publication. We follow the rule that excluding a + * partition root means that the entire partition tree is excluded. Thus, + * check whether the root is in the EXCEPT clause and reject adding the + * individual partition to the publication. + */ + if (targetrel->rd_rel->relispartition) + { + List *ancestors; + + if (pri->except) + { + /* If in EXCEPT clause, must be root partitioned table */ + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg(errormsg, relname), + errdetail("This operation is not supported for individual partitions."))); + } + + /* + * ancestors is NIL for a partition with a pending DETACH + * CONCURRENTLY, in which case there is no root to check. + */ + ancestors = get_partition_ancestors(RelationGetRelid(targetrel)); + if (ancestors != NIL) + { + Oid root = llast_oid(ancestors); + bool root_except; + + if (CheckPublicationRelEntry(pubid, root, &root_except) && root_except) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot add partition \"%s\" to publication \"%s\"", + RelationGetQualifiedRelationName(targetrel), + get_publication_name(pubid, false)), + errdetail("Partition root \"%s\" is named in the publication's EXCEPT clause for schema \"%s\".", + quote_qualified_identifier(get_namespace_name(get_rel_namespace(root)), + get_rel_name(root)), + get_namespace_name(get_rel_namespace(root))), + errhint("Change the EXCEPT clause using ALTER PUBLICATION ... SET TABLES IN SCHEMA ... EXCEPT.")); + } + + list_free(ancestors); + } /* Must be a regular or partitioned table */ if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION && @@ -320,8 +359,7 @@ is_schema_publication(Oid pubid) } /* - * Returns true if the publication has explicitly included relation (i.e., - * not marked as EXCEPT). + * Returns true if the publication has a FOR TABLE clause. */ bool is_table_publication(Oid pubid) @@ -341,19 +379,23 @@ is_table_publication(Oid pubid) scan = systable_beginscan(pubrelsrel, PublicationRelPrpubidIndexId, true, NULL, 1, &scankey); - tup = systable_getnext(scan); - if (HeapTupleIsValid(tup)) - { - Form_pg_publication_rel pubrel; - - pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); - /* - * For any publication, pg_publication_rel contains either only EXCEPT - * entries or only explicitly included tables. Therefore, examining - * the first tuple is sufficient to determine table inclusion. - */ - result = !pubrel->prexcept; + /* + * A publication can have both explicitly included (prexcept = false) and + * EXCEPT (prexcept = true) rows for the same pubid. For example: + * + * CREATE PUBLICATION pub1 FOR TABLE s1.t1, TABLES IN SCHEMA s2 EXCEPT + * (TABLE s2.t1); + * + * Scan until we find an explicitly included row. + */ + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + if (!((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept) + { + result = true; + break; + } } systable_endscan(scan); @@ -362,6 +404,27 @@ is_table_publication(Oid pubid) return result; } +/* + * Check whether a pg_publication_rel row exists for (pubid, relid); if so, + * set *is_except to its prexcept flag. + */ +bool +CheckPublicationRelEntry(Oid pubid, Oid relid, bool *is_except) +{ + HeapTuple tup; + + tup = SearchSysCache2(PUBLICATIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(pubid)); + if (!HeapTupleIsValid(tup)) + return false; + + *is_except = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept; + ReleaseSysCache(tup); + + return true; +} + /* * Returns true if the relation has column list associated with the * publication, false otherwise. @@ -457,11 +520,26 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt, * ancestor is at the end of the list. */ Oid -GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level) +GetTopMostAncestorInPublication(Oid puboid, List *ancestors, + int *ancestor_level) { ListCell *lc; Oid topmost_relid = InvalidOid; int level = 0; + bool root_except; + + if (ancestors == NIL) + return InvalidOid; + + /* + * If the partition root is excluded from this publication via an EXCEPT + * clause, the partition is not published through this publication, so + * return InvalidOid. Since only partition roots can appear in an EXCEPT + * clause, the root is the only ancestor worth a catalog lookup. + */ + if (CheckPublicationRelEntry(puboid, llast_oid(ancestors), &root_except) && + root_except) + return InvalidOid; /* * Find the "topmost" ancestor that is in this publication. @@ -548,6 +626,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, List *relids = NIL; int i; bool inval_except_table; + bool is_except; rel = table_open(PublicationRelRelationId, RowExclusiveLock); @@ -556,21 +635,46 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, * duplicates, it's here just to provide nicer error message in common * case. The real protection is the unique key on the catalog. */ - if (SearchSysCacheExists2(PUBLICATIONRELMAP, ObjectIdGetDatum(relid), - ObjectIdGetDatum(pubid))) + if (CheckPublicationRelEntry(pubid, relid, &is_except)) { table_close(rel, RowExclusiveLock); - if (if_not_exists) + /* + * if_not_exists asks us to skip an entry that is already present. An + * existing entry of the opposite kind is not the entry the caller + * asked for, though: skipping it would silently leave the relation + * excluded when the caller wanted it published, or published when the + * caller wanted it excluded. So only skip when the existing entry is + * of the same kind, and report the conflict otherwise. + */ + if (if_not_exists && is_except == pri->except) return InvalidObjectAddress; - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_OBJECT), - errmsg("relation \"%s\" is already a member of publication \"%s\"", - RelationGetRelationName(targetrel), pub->name))); + if (is_except) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot add table \"%s\" to publication \"%s\"", + RelationGetQualifiedRelationName(targetrel), + pub->name), + errdetail("The table is named in the publication's EXCEPT clause for schema \"%s\".", + get_namespace_name(RelationGetNamespace(targetrel))), + errhint("Change the EXCEPT clause using ALTER PUBLICATION ... SET TABLES IN SCHEMA ... EXCEPT."))); + else if (pri->except) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot exclude table \"%s\" from publication \"%s\"", + RelationGetQualifiedRelationName(targetrel), + pub->name), + errdetail("The table is a member of the publication."), + errhint("Remove the table from the publication using ALTER PUBLICATION ... DROP TABLE."))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("relation \"%s\" is already a member of publication \"%s\"", + RelationGetRelationName(targetrel), pub->name))); } - check_publication_add_relation(pri); + check_publication_add_relation(pri, pubid); /* Validate and translate column names into a Bitmapset of attnums. */ attnums = pub_collist_validate(pri->relation, pri->columns); @@ -996,16 +1100,36 @@ GetIncludedPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) * Gets list of table oids that were specified in the EXCEPT clause for a * publication. * - * This should only be used FOR ALL TABLES publications. + * This is used for FOR ALL TABLES and FOR TABLES IN SCHEMA publications, + * both of which support EXCEPT TABLE. */ 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); } +/* + * Gets list of relation oids associated with a publication, covering both + * explicitly included relations and relations named in an EXCEPT clause. + * + * This is used by ALTER PUBLICATION ... SET to replaces a publication's entire + * relation list. + */ +List * +GetPublicationRelationsOfAnyKind(Oid pubid, PublicationPartOpt pub_partopt) +{ + List *result; + + result = get_publication_relations(pubid, pub_partopt, false); + + return list_concat_unique_oid(result, + get_publication_relations(pubid, pub_partopt, + true)); +} + /* * Gets list of publication oids for publications marked as FOR ALL TABLES. */ @@ -1246,22 +1370,63 @@ GetSchemaPublicationRelations(Oid schemaid, PublicationPartOpt pub_partopt) /* * Gets the list of all relations published by FOR TABLES IN SCHEMA - * publication. + * publication, excluding any tables listed in EXCEPT clauses. */ List * GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) { List *result = NIL; List *pubschemalist = GetPublicationSchemas(pubid); + List *except_relids = NIL; ListCell *cell; + /* get the list of tables excluded via EXCEPT TABLE for this publication */ + if (pubschemalist != NIL) + except_relids = GetExcludedPublicationTables(pubid, pub_partopt); + foreach(cell, pubschemalist) { Oid schemaid = lfirst_oid(cell); List *schemaRels = NIL; schemaRels = GetSchemaPublicationRelations(schemaid, pub_partopt); - result = list_concat(result, schemaRels); + + if (except_relids != NIL) + { + /* filter out any tables that appear in the EXCEPT list */ + foreach_oid(relid, schemaRels) + { + bool excluded = list_member_oid(except_relids, relid); + + /* + * A partition whose root is excluded from the publication is + * also excluded, even if the partition itself lives in a + * different (included) schema. Only the topmost root of a + * partition hierarchy can ever appear in EXCEPT (see + * check_publication_add_relation()), so it's enough to check + * the last element of the ancestors list, rather than walking + * the whole chain. + */ + if (!excluded && get_rel_relispartition(relid)) + { + List *ancestors = get_partition_ancestors(relid); + + if (ancestors != NIL) + { + excluded = list_member_oid(except_relids, + llast_oid(ancestors)); + list_free(ancestors); + } + } + + if (!excluded) + result = lappend_oid(result, relid); + } + + list_free(schemaRels); + } + else + result = list_concat(result, schemaRels); } return result; @@ -1338,6 +1503,7 @@ is_table_publishable_in_publication(Oid relid, Publication *pub) { bool relispartition; List *ancestors = NIL; + bool is_except; /* * For non-pubviaroot publications, a partitioned table is never the @@ -1385,6 +1551,10 @@ is_table_publishable_in_publication(Oid relid, Publication *pub) */ /* + * If the partition root is excluded via the EXCEPT clause, the partition + * is never considered to be published through this publication. Check + * this before checking whether an ancestor is published. + * * If an ancestor is published, the partition's status depends on * publish_via_partition_root value. * @@ -1394,20 +1564,38 @@ 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 && - OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL))) - return !pub->pubviaroot; + if (relispartition) + { + /* + * ancestors can be NIL for a partition whose DETACH ... CONCURRENTLY + * is pending, in which case there is no ancestor to check. + */ + if (ancestors != NIL && + CheckPublicationRelEntry(pub->oid, llast_oid(ancestors), &is_except) && + is_except) + return false; + + if (OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL))) + return !pub->pubviaroot; + } /* * Check whether the table is explicitly published via pg_publication_rel * or pg_publication_namespace. + * + * A pg_publication_rel row with prexcept=true means the table is + * explicitly excluded via EXCEPT and must not be reported as published, + * even if its schema is otherwise included. A row with prexcept=false + * means it is explicitly included. If no pg_publication_rel row exists, + * the table is published iff its schema appears in + * pg_publication_namespace. */ - return (SearchSysCacheExists2(PUBLICATIONRELMAP, - ObjectIdGetDatum(relid), - ObjectIdGetDatum(pub->oid)) || - SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP, - ObjectIdGetDatum(get_rel_namespace(relid)), - ObjectIdGetDatum(pub->oid))); + if (CheckPublicationRelEntry(pub->oid, relid, &is_except)) + return !is_except; + else + return SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP, + ObjectIdGetDatum(get_rel_namespace(relid)), + ObjectIdGetDatum(pub->oid)); } /* diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index 96838730fe1..77a829bc1bd 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -22,6 +22,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/objectaddress.h" +#include "catalog/partition.h" #include "catalog/pg_database.h" #include "catalog/pg_inherits.h" #include "catalog/pg_namespace.h" @@ -61,7 +62,7 @@ typedef struct rf_context Oid parentid; /* relid of the parent relation */ } rf_context; -static List *OpenTableList(List *tables); +static List *OpenTableList(List *tables, List **cross_schema_children); static void CloseTableList(List *rels); static void LockSchemaList(List *schemalist); static void PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, @@ -71,6 +72,16 @@ static void PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists, AlterPublicationStmt *stmt); static void PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok); 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, + List *except_rels); +static void ProcessSchemaExceptTables(Oid schemaid, List *except_tables, + ParseState *pstate, List **schemas, + List **schemas_with_except, + List **except_pubtables); static void @@ -175,16 +186,93 @@ parse_publication_options(ParseState *pstate, } } +/* + * Reject a schema being mentioned more than once with an EXCEPT clause, even + * if the EXCEPT clauses are identical — much like OpenTableList() rejects + * "FOR TABLE t1(a), t1(a)" despite the column lists matching. A schema can + * still be mentioned multiple times, just not more than once with EXCEPT. + * + * Also qualify unqualified EXCEPT table names with the given schema (rejecting + * any explicitly qualified with a different schema), and append them to + * *except_pubtables. + * + * schemaid: OID of the schema for this TABLES IN SCHEMA mention. + * except_tables: the EXCEPT list (or NIL) attached to this mention. + * pstate: parse state of the statement, used to report the error position of + * an offending EXCEPT entry. + * *schemas: accumulates all schema OIDs seen so far in this statement. + * *schemas_with_except: This is a subset of *schemas. Tracks which of + * the seen schemas of this statement had an EXCEPT clause. + * *except_pubtables: accumulates the (now schema-qualified) EXCEPT table + * entries across the whole statement. + */ +static void +ProcessSchemaExceptTables(Oid schemaid, List *except_tables, + ParseState *pstate, List **schemas, + List **schemas_with_except, List **except_pubtables) +{ + char *schema_name = get_namespace_name(schemaid); + bool schema_has_except = (except_tables != NIL); + + /* + * A repeating schema is only a problem if this mention has EXCEPT, or an + * earlier mention of the same schema did. + */ + if (list_member_oid(*schemas, schemaid)) + { + if (schema_has_except || + list_member_oid(*schemas_with_except, schemaid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("conflicting or redundant EXCEPT clauses for schema \"%s\"", + schema_name)); + } + + if (schema_has_except) + *schemas_with_except = lappend_oid(*schemas_with_except, schemaid); + + /* Filter out duplicates if the user specifies "sch1, sch1" */ + *schemas = list_append_unique_oid(*schemas, schemaid); + + if (!schema_has_except) + return; + + foreach_ptr(PublicationObjSpec, eobj, except_tables) + { + RangeVar *relation = eobj->pubtable->relation; + + if (relation->schemaname == NULL) + relation->schemaname = schema_name; + else if (strcmp(relation->schemaname, schema_name) != 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("table \"%s\" in EXCEPT clause does not belong to schema \"%s\"", + quote_qualified_identifier(relation->schemaname, relation->relname), + schema_name), + parser_errposition(pstate, eobj->location)); + + /* + * Remember that this EXCEPT entry is associated with a schema. This is + * used by OpenTableList() to skip inheritance children outside that + * schema. + */ + eobj->pubtable->except_in_schema = true; + + *except_pubtables = lappend(*except_pubtables, eobj->pubtable); + } +} + /* * Convert the PublicationObjSpecType list into schema oid list and * PublicationTable list. */ static void ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, - List **rels, List **exceptrels, List **schemas) + List **rels, List **except_pubtables, List **schemas) { ListCell *cell; PublicationObjSpec *pubobj; + List *schemas_with_except = NIL; if (!pubobjspec_list) return; @@ -200,7 +288,7 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, { case PUBLICATIONOBJ_EXCEPT_TABLE: pubobj->pubtable->except = true; - *exceptrels = lappend(*exceptrels, pubobj->pubtable); + *except_pubtables = lappend(*except_pubtables, pubobj->pubtable); break; case PUBLICATIONOBJ_TABLE: pubobj->pubtable->except = false; @@ -209,8 +297,10 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, case PUBLICATIONOBJ_TABLES_IN_SCHEMA: schemaid = get_namespace_oid(pubobj->name, false); - /* Filter out duplicates if user specifies "sch1, sch1" */ - *schemas = list_append_unique_oid(*schemas, schemaid); + ProcessSchemaExceptTables(schemaid, pubobj->except_tables, + pstate, schemas, + &schemas_with_except, + except_pubtables); break; case PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA: search_path = fetch_search_path(false); @@ -222,8 +312,10 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, schemaid = linitial_oid(search_path); list_free(search_path); - /* Filter out duplicates if user specifies "sch1, sch1" */ - *schemas = list_append_unique_oid(*schemas, schemaid); + ProcessSchemaExceptTables(schemaid, pubobj->except_tables, + pstate, schemas, + &schemas_with_except, + except_pubtables); break; default: /* shouldn't happen */ @@ -770,6 +862,133 @@ TransformPubWhereClauses(List *tables, const char *queryString, } } +/* + * Check that a table is not both excluded and published in the same DDL. + * + * Similar checks are present in publication_add_relation() and + * check_publication_add_relation(), but they rely on pg_publication_rel + * entries and ancestor lookups. Here, the checks are performed on the + * object lists collected during the statement, as the catalog state is + * not sufficient in the cases below: + * + * - CreatePublication() inserts the explicit tables and the EXCEPT tables in + * a single command, but rows inserted by the current command are not + * visible to later lookups within the same command. + * + * - For partitions, the excluded root's row might not yet exist because the + * root may be added to the EXCEPT list later. Detecting such contradictions + * when adding the root to the EXCEPT clause would require traversing + * downward through the partition hierarchy, whereas the existing ancestor + * lookups only traverse upward from the relation being added. + */ +static void +CheckExceptNotInTableList(List *except_rels, List *cross_schema_children, + List *explicitrelids) +{ + foreach_oid(explicitrelid, explicitrelids) + { + Oid root = InvalidOid; + + if (get_rel_relispartition(explicitrelid)) + { + List *ancestors = get_partition_ancestors(explicitrelid); + + /* + * ancestors is NIL for a partition with a pending DETACH + * CONCURRENTLY. + */ + if (ancestors != NIL) + root = llast_oid(ancestors); + list_free(ancestors); + } + + foreach_ptr(PublicationRelInfo, pri, except_rels) + { + Oid exceptrelid = RelationGetRelid(pri->relation); + + if (exceptrelid == explicitrelid) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table \"%s\" cannot be both published and excluded", + RelationGetQualifiedRelationName(pri->relation))); + + if (OidIsValid(root) && exceptrelid == root) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("partition \"%s\" cannot be both published and excluded", + quote_qualified_identifier(get_namespace_name(get_rel_namespace(explicitrelid)), + get_rel_name(explicitrelid))), + errdetail("Partition root \"%s\" is named in the publication's EXCEPT clause for schema \"%s\".", + RelationGetQualifiedRelationName(pri->relation), + get_namespace_name(RelationGetNamespace(pri->relation))))); + } + + /* + * An EXCEPT clause also applies to inheritance children of a parent + * named without ONLY. Children in other schemas are not added to + * except_rels, but are still excluded by the EXCEPT clause. Explicitly + * publishing such a child therefore conflicts with the EXCEPT clause. + */ + if (list_member_oid(cross_schema_children, explicitrelid)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table \"%s\" cannot be both published and excluded", + quote_qualified_identifier(get_namespace_name(get_rel_namespace(explicitrelid)), + get_rel_name(explicitrelid))), + errdetail("It inherits from a table named in an EXCEPT clause of this statement."), + errhint("Use ONLY in the EXCEPT clause to exclude just the parent, or do not publish this table explicitly.")); + } +} + +/* + * Check that an EXCEPT clause does not conflict with a TABLES IN SCHEMA + * clause in the same statement. + * + * 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. + * + * 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. + * + * Conflicts with explicitly listed tables are handled by + * CheckExceptNotInTableList(). + */ +static void +CheckExceptChildNotInSchemaList(List *cross_schema_children, List *schemaidlist, + List *except_rels) +{ + foreach_oid(childrelid, cross_schema_children) + { + Oid childnsp = get_rel_namespace(childrelid); + bool excluded = false; + + if (!list_member_oid(schemaidlist, childnsp)) + continue; + + foreach_ptr(PublicationRelInfo, pri, except_rels) + { + if (RelationGetRelid(pri->relation) == childrelid) + { + excluded = true; + break; + } + } + + if (excluded) + continue; + + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table \"%s\" cannot be both published and excluded", + quote_qualified_identifier(get_namespace_name(childnsp), + 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\".", + get_namespace_name(childnsp))); + } +} /* * Given a list of tables that are going to be added to a publication, @@ -849,7 +1068,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) char publish_generated_columns; AclResult aclresult; List *relations = NIL; - List *exceptrelations = NIL; + List *except_pubtables = NIL; List *schemaidlist = NIL; /* must have CREATE privilege on database */ @@ -936,16 +1155,16 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) /* Associate objects with the publication. */ ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, - &exceptrelations, &schemaidlist); + &except_pubtables, &schemaidlist); if (stmt->for_all_tables) { /* Process EXCEPT table list */ - if (exceptrelations != NIL) + if (except_pubtables != NIL) { List *rels; - rels = OpenTableList(exceptrelations); + rels = OpenTableList(except_pubtables, NULL); PublicationAddTables(puboid, rels, true, NULL); CloseTableList(rels); } @@ -959,6 +1178,11 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) } else if (!stmt->for_all_sequences) { + List *explicitrelids = NIL; + + /* EXCEPT tables here always belong to a schema in schemaidlist */ + Assert(except_pubtables == NIL || schemaidlist != NIL); + /* FOR TABLES IN SCHEMA requires superuser */ if (schemaidlist != NIL && !superuser()) ereport(ERROR, @@ -969,7 +1193,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) { List *rels; - rels = OpenTableList(relations); + rels = OpenTableList(relations, NULL); TransformPubWhereClauses(rels, pstate->p_sourcetext, publish_via_partition_root); @@ -978,6 +1202,20 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) publish_via_partition_root); PublicationAddTables(puboid, rels, true, NULL); + + /* + * Collect explicit table OIDs now, before we close the relation + * list, so that the except-table validation for pub-included + * schemas below can check for contradictions without relying on a + * catalog scan that might not yet see the just-inserted rows. + */ + if (except_pubtables != NIL) + { + foreach_ptr(PublicationRelInfo, pri, rels) + explicitrelids = lappend_oid(explicitrelids, + RelationGetRelid(pri->relation)); + } + CloseTableList(rels); } @@ -989,6 +1227,32 @@ 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); + } } } @@ -1255,7 +1519,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, if (!tables && stmt->action != AP_SetObjects) return; - rels = OpenTableList(tables); + rels = OpenTableList(tables, NULL); if (stmt->action == AP_AddObjects) { @@ -1295,8 +1559,15 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, } else { - oldrelids = GetIncludedPublicationRelations(pubid, - PUBLICATION_PART_ROOT); + /* + * SET replaces the publication's whole relation list, so every + * existing entry has to be reconciled against the new one -- an + * EXCEPT entry included. If the same relation is now requested + * with the opposite kind, the old entry is dropped below and the + * new one added, rather than the old one being silently kept. + */ + oldrelids = GetPublicationRelationsOfAnyKind(pubid, + PUBLICATION_PART_ROOT); TransformPubWhereClauses(rels, queryString, pubform->pubviaroot); @@ -1317,6 +1588,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, HeapTuple rftuple; Node *oldrelwhereclause = NULL; Bitmapset *oldcolumns = NULL; + bool oldexcept = false; /* look up the cache for the old relmap */ rftuple = SearchSysCache2(PUBLICATIONRELMAP, @@ -1348,6 +1620,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, if (!isnull) oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL); + oldexcept = ((Form_pg_publication_rel) GETSTRUCT(rftuple))->prexcept; + ReleaseSysCache(rftuple); } @@ -1371,14 +1645,17 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, /* * Check if any of the new set of relations matches with the - * existing relations in the publication. Additionally, if the + * existing relations in the publication. An entry matches + * only if it is of the same kind, that is, both are ordinary + * members or both are EXCEPT entries. Additionally, if the * relation has an associated WHERE clause, check the WHERE * expressions also match. Same for the column list. Drop the * rest. */ if (newrelid == oldrelid) { - if (equal(oldrelwhereclause, newpubrel->whereClause) && + if (newpubrel->except == oldexcept && + equal(oldrelwhereclause, newpubrel->whereClause) && bms_equal(oldcolumns, newcolumns)) { found = true; @@ -1829,13 +2106,69 @@ RemovePublicationSchemaById(Oid psoid) table_close(rel, RowExclusiveLock); } +/* + * Remove any EXCEPT clause entries for a relation from schema publications. + * Called when a table changes schema (ALTER TABLE ... SET SCHEMA), so that + * a schema-scoped exclusion does not silently follow the table to its new + * schema. + */ +void +RemoveSchemaPubExceptForRel(Oid relid, Oid oldNspOid, Oid newNspOid) +{ + List *pubids; + + /* + * If the table is not actually moving to a different schema (no-op ALTER + * TABLE ... SET SCHEMA ), there is nothing to do. + */ + if (oldNspOid == newNspOid) + return; + + pubids = GetRelationExcludedPublications(relid); + + foreach_oid(pubid, pubids) + { + Oid proid; + + /* + * This problem does not apply to FOR ALL TABLES publications, because + * their EXCEPT clause is publication-scoped, not schema-scoped: the + * exclusion should persist regardless of what schema the table is in. + */ + if (!is_schema_publication(pubid)) + continue; + + proid = GetSysCacheOid2(PUBLICATIONRELMAP, + Anum_pg_publication_rel_oid, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(pubid)); + if (OidIsValid(proid)) + { + ObjectAddress obj; + + ObjectAddressSet(obj, PublicationRelRelationId, proid); + + ereport(DEBUG2, + errmsg_internal("auto-remove exclusion of table \"%s.%s\" from publication \"%s\": table moved to schema \"%s\"", + get_namespace_name(oldNspOid), + get_rel_name(relid), + get_publication_name(pubid, false), + get_namespace_name(newNspOid))); + + performDeletion(&obj, DROP_CASCADE, 0); + } + } + + list_free(pubids); +} + /* * Open relations specified by a PublicationTable list. * The returned tables are locked in ShareUpdateExclusiveLock mode in order to * add them to a publication. */ static List * -OpenTableList(List *tables) +OpenTableList(List *tables, List **cross_schema_children) { List *relids = NIL; List *rels = NIL; @@ -1911,6 +2244,7 @@ OpenTableList(List *tables) { List *children; ListCell *child; + Oid parentnsp = RelationGetNamespace(rel); children = find_all_inheritors(myrelid, ShareUpdateExclusiveLock, NULL); @@ -1922,6 +2256,21 @@ OpenTableList(List *tables) /* Allow query cancel in case this takes a long time */ CHECK_FOR_INTERRUPTS(); + /* + * An EXCEPT clause also excludes inheritance children in other + * schemas. Keep track of such children separately so the + * caller can detect if the same statement also includes them + * through another table or schema clause. + */ + if (t->except_in_schema && + get_rel_namespace(childrelid) != parentnsp) + { + if (cross_schema_children) + *cross_schema_children = lappend_oid(*cross_schema_children, + childrelid); + continue; + } + /* * Skip duplicates if user specified both parent and child * tables. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 8349e724c2b..64c49f69123 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -49,6 +49,7 @@ #include "catalog/pg_opclass.h" #include "catalog/pg_policy.h" #include "catalog/pg_proc.h" +#include "catalog/pg_publication.h" #include "catalog/pg_publication_rel.h" #include "catalog/pg_rewrite.h" #include "catalog/pg_statistic_ext.h" @@ -63,6 +64,7 @@ #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/repack.h" +#include "commands/publicationcmds.h" #include "commands/sequence.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -19803,6 +19805,16 @@ AlterTableNamespaceInternal(Relation rel, Oid oldNspOid, Oid nspOid, AlterConstraintNamespaces(RelationGetRelid(rel), oldNspOid, nspOid, false, objsMoved); + /* + * Remove any EXCEPT clause entries for this relation from schema + * publications. A schema-scoped exclusion is no longer meaningful once + * the table moves to a different schema. + */ + if (rel->rd_rel->relkind == RELKIND_RELATION || + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + RemoveSchemaPubExceptForRel(RelationGetRelid(rel), oldNspOid, + nspOid); + table_close(classRel, RowExclusiveLock); } @@ -21136,6 +21148,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, if (exceptpuboids != NIL) { bool first = true; + bool has_alltables_pub = false; StringInfoData pubnames; initStringInfo(&pubnames); @@ -21149,17 +21162,22 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, else appendStringInfo(&pubnames, _(", \"%s\""), pubname); first = false; + + if (GetPublication(pubid)->alltables) + has_alltables_pub = true; } ereport(ERROR, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg_plural("cannot attach table \"%s\" as partition because it is referenced in publication %s EXCEPT clause", - "cannot attach table \"%s\" as partition because it is referenced in publications %s EXCEPT clause", + errmsg_plural("cannot attach table \"%s\" as partition because it is named in EXCEPT clause of publication %s", + "cannot attach table \"%s\" as partition because it is named in EXCEPT clause of publications %s", list_length(exceptpuboids), RelationGetRelationName(attachrel), pubnames.data), errdetail("The publication EXCEPT clause cannot contain tables that are partitions."), - errhint("Change the publication's EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES.")); + errhint("%s", has_alltables_pub ? + _("Change the EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES ... EXCEPT.") : + _("Change the EXCEPT clause using ALTER PUBLICATION ... SET TABLES IN SCHEMA ... EXCEPT."))); } list_free(exceptpuboids); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 17035fb4d15..14520a5ccab 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -58,6 +58,7 @@ #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parser.h" +#include "utils/builtins.h" #include "utils/datetime.h" #include "utils/xml.h" @@ -11282,7 +11283,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec * pub_obj is one of: * * TABLE table [, ...] - * TABLES IN SCHEMA schema [, ...] + * TABLES IN SCHEMA schema [EXCEPT (TABLE table [, ...] )] [, ...] * *****************************************************************************/ @@ -11342,23 +11343,26 @@ PublicationObjSpec: $$->pubtable->columns = $3; $$->pubtable->whereClause = $4; } - | TABLES IN_P SCHEMA ColId + | TABLES IN_P SCHEMA ColId opt_pub_except_clause { $$ = makeNode(PublicationObjSpec); $$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA; $$->name = $4; + $$->except_tables = $5; $$->location = @4; } - | TABLES IN_P SCHEMA CURRENT_SCHEMA + | TABLES IN_P SCHEMA CURRENT_SCHEMA opt_pub_except_clause { $$ = makeNode(PublicationObjSpec); $$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA; + $$->except_tables = $5; $$->location = @4; } - | ColId opt_column_list OptWhereClause + | ColId opt_column_list OptWhereClause opt_pub_except_clause { $$ = makeNode(PublicationObjSpec); $$->pubobjtype = PUBLICATIONOBJ_CONTINUATION; + $$->except_tables = $4; /* * If either a row filter or column list is specified, create * a PublicationTable object. @@ -11402,10 +11406,11 @@ PublicationObjSpec: $$->pubtable->columns = $2; $$->pubtable->whereClause = $3; } - | CURRENT_SCHEMA + | CURRENT_SCHEMA opt_pub_except_clause { $$ = makeNode(PublicationObjSpec); $$->pubobjtype = PUBLICATIONOBJ_CONTINUATION; + $$->except_tables = $2; $$->location = @1; } ; @@ -20867,6 +20872,11 @@ preprocess_pub_all_objtype_list(List *all_objects_list, List **pubobjects, /* * Process pubobjspec_list to check for errors in any of the objects and * convert PUBLICATIONOBJ_CONTINUATION into appropriate PublicationObjSpecType. + * + * The except_tables attached to TABLES IN SCHEMA nodes are left in place here; + * ObjectsInPublicationToOids() qualifies their names, validates schema + * membership, and merges the qualified tables into its except_pubtables + * output list once the schema OID is known. */ static void preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner) @@ -20895,6 +20905,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner) if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE) { + /* EXCEPT is not valid for table objects */ + if (pubobj->except_tables != NIL) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("EXCEPT is not allowed for TABLE publication objects"), + parser_errposition(pubobj->location)); + /* relation name or pubtable must be set for this type of object */ if (!pubobj->name && !pubobj->pubtable) ereport(ERROR, diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c index 0afdb1432ca..ee2b997f82d 100644 --- a/src/backend/replication/pgoutput/pgoutput.c +++ b/src/backend/replication/pgoutput/pgoutput.c @@ -2281,10 +2281,41 @@ get_rel_sync_entry(PGOutputData *data, Relation relation) } } - if (list_member_oid(pubids, pub->oid) || - list_member_oid(schemaPubids, pub->oid) || - ancestor_published) + if (list_member_oid(pubids, pub->oid) || ancestor_published) publish = true; + else if (list_member_oid(schemaPubids, pub->oid)) + { + Oid root_relid; + bool root_is_except = false; + + /* + * schemaPubids is the list of publications that include + * relid's own schema, independent of the ancestor walk + * above -- a partition can live in a different schema + * than its root. Still need to check for exclusion here, + * using the top-most ancestor since only a root + * (non-partition) table can appear in an EXCEPT clause. + */ + if (am_partition) + { + List *ancestors = get_partition_ancestors(relid); + + /* + * ancestors is NIL for a partition with a pending + * DETACH CONCURRENTLY; + */ + root_relid = (ancestors == NIL) ? relid : + llast_oid(ancestors); + list_free(ancestors); + } + else + root_relid = relid; + + CheckPublicationRelEntry(pub->oid, root_relid, &root_is_except); + + if (!root_is_except) + publish = true; + } } /* diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 19c4ff6e75e..e4eb5e6d885 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -5807,7 +5807,6 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) { List *puboids = NIL; List *exceptpuboids = NIL; - List *alltablespuboids; ListCell *lc; MemoryContext oldcxt; Oid schemaid; @@ -5851,12 +5850,9 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) if (relation->rd_rel->relispartition) { - Oid last_ancestor_relid; - - /* Add publications that the ancestors are in too. */ ancestors = get_partition_ancestors(relid); - last_ancestor_relid = llast_oid(ancestors); + /* Add publications that the ancestors are in too. */ foreach(lc, ancestors) { Oid ancestor = lfirst_oid(lc); @@ -5869,11 +5865,13 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) } /* - * Only the top-most ancestor can appear in the EXCEPT clause. - * Therefore, for a partition, exclusion must be evaluated at the - * top-most ancestor. + * Only the topmost root of a partition hierarchy can appear in an + * EXCEPT clause, so that is where exclusion has to be evaluated. + * ancestors is NIL for a partition with a pending DETACH + * CONCURRENTLY, in which case there is no root to consult. */ - exceptpuboids = GetRelationExcludedPublications(last_ancestor_relid); + if (ancestors != NIL) + exceptpuboids = GetRelationExcludedPublications(llast_oid(ancestors)); } else { @@ -5884,10 +5882,12 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) exceptpuboids = GetRelationExcludedPublications(relid); } - alltablespuboids = GetAllTablesPublications(); - puboids = list_concat_unique_oid(puboids, - list_difference_oid(alltablespuboids, - exceptpuboids)); + puboids = list_concat_unique_oid(puboids, GetAllTablesPublications()); + + /* Ignore the publications that exclude this relation. */ + if (exceptpuboids != NIL) + puboids = list_difference_oid(puboids, exceptpuboids); + foreach(lc, puboids) { Oid pubid = lfirst_oid(lc); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index afe4b323a7b..51f70aaff23 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3036,7 +3036,29 @@ describeOneTableDetails(const char *schemaname, "FROM pg_catalog.pg_publication p\n" " JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n" " JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n" - "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n" + "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n", + oid, oid); + + if (pset.sversion >= 200000) + { + /* + * Publishing the schema does not publish a table named in the + * publication's EXCEPT clause, nor a partition whose root is + * named there, since excluding a root excludes the whole + * partition tree. Unlike the FOR ALL TABLES case below, a + * schema publication can have both kinds of + * pg_publication_rel row, so prexcept has to be tested. + */ + appendPQExpBuffer(&buf, + " AND NOT EXISTS (\n" + " SELECT 1\n" + " FROM pg_catalog.pg_publication_rel pr\n" + " WHERE pr.prpubid = p.oid AND pr.prexcept AND\n" + " (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s')))\n", + oid, oid); + } + + appendPQExpBuffer(&buf, "UNION\n" "SELECT pubname\n" " , pg_catalog.pg_get_expr(pr.prqual, c.oid)\n" @@ -3060,7 +3082,7 @@ describeOneTableDetails(const char *schemaname, " FROM pg_catalog.pg_publication_namespace pn\n" " WHERE pn.pnpubid = p.oid\n" " AND pn.pnnspid = c.relnamespace)\n", - oid, oid, oid); + oid); if (pset.sversion >= 190000) { @@ -6839,6 +6861,24 @@ describePublications(const char *pattern) if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"), true, &cont)) goto error_return; + + if (pset.sversion >= 200000) + { + /* + * Get tables in the EXCEPT clause for this schema + * publication. + */ + printfPQExpBuffer(&buf, + "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n" + "FROM pg_catalog.pg_class c\n" + " JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n" + "WHERE pr.prpubid = '%s'\n" + " AND pr.prexcept\n" + "ORDER BY 1", pubid); + if (!addFooterToPublicationDesc(&buf, _("Except tables:"), + true, &cont)) + goto error_return; + } } } else diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 190fff7ea0e..f069e2654cc 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1065,6 +1065,24 @@ static const SchemaQuery Query_for_trigger_of_table = { "SELECT nspname FROM pg_catalog.pg_namespace "\ " WHERE nspname LIKE '%s'" +#define Query_for_list_of_tables_in_schema \ +"SELECT n.nspname || '.' || c.relname "\ +" FROM pg_catalog.pg_class c "\ +" JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "\ +" WHERE c.relkind IN (" CppAsString2(RELKIND_RELATION) ", " \ + CppAsString2(RELKIND_PARTITIONED_TABLE) ") "\ +" AND (n.nspname || '.' || c.relname) LIKE '%s' "\ +" AND n.nspname = '%s'" + +#define Query_for_list_of_tables_in_current_schema \ +"SELECT c.relname "\ +" FROM pg_catalog.pg_class c "\ +" JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "\ +" WHERE c.relkind IN (" CppAsString2(RELKIND_RELATION) ", " \ + CppAsString2(RELKIND_PARTITIONED_TABLE) ") "\ +" AND c.relname LIKE '%s' "\ +" AND n.nspname = pg_catalog.current_schema()" + /* Use COMPLETE_WITH_QUERY_VERBATIM with these queries for GUC names: */ #define Query_for_list_of_alter_system_set_vars \ "SELECT pg_catalog.lower(name) FROM pg_catalog.pg_settings "\ @@ -3777,8 +3795,21 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas " AND nspname NOT LIKE E'pg\\\\_%%'", "CURRENT_SCHEMA"); - else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", MatchAny) && (!ends_with(prev_wd, ','))) - COMPLETE_WITH("WITH ("); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", MatchAny) && !ends_with(prev_wd, ',')) + COMPLETE_WITH("EXCEPT ( TABLE", "WITH ("); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT")) + COMPLETE_WITH("( TABLE"); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(")) + COMPLETE_WITH("TABLE"); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", "CURRENT_SCHEMA", "EXCEPT", "(", "TABLE")) + COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_tables_in_current_schema); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "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("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE", MatchAnyN) && !ends_with(prev_wd, ',')) + COMPLETE_WITH(")"); /* Complete "CREATE PUBLICATION [...] WITH" */ else if (Matches("CREATE", "PUBLICATION", MatchAnyN, "WITH", "(")) COMPLETE_WITH("publish", "publish_generated_columns", "publish_via_partition_root"); diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h index 89b4bb14f62..451559552ad 100644 --- a/src/include/catalog/pg_publication.h +++ b/src/include/catalog/pg_publication.h @@ -179,6 +179,8 @@ extern List *GetIncludedPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt); extern List *GetExcludedPublicationTables(Oid pubid, PublicationPartOpt pub_partopt); +extern List *GetPublicationRelationsOfAnyKind(Oid pubid, + PublicationPartOpt pub_partopt); extern List *GetAllTablesPublications(void); extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot); extern List *GetPublicationSchemas(Oid pubid); @@ -192,6 +194,7 @@ extern List *GetPubPartitionOptionRelations(List *result, Oid relid); extern Oid GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level); +extern bool CheckPublicationRelEntry(Oid pubid, Oid relid, bool *is_except); extern bool is_publishable_relation(Relation rel); extern bool is_schema_publication(Oid pubid); diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h index 4cf45c17cc5..fb1703aa675 100644 --- a/src/include/commands/publicationcmds.h +++ b/src/include/commands/publicationcmds.h @@ -27,6 +27,8 @@ extern void AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt); extern void RemovePublicationById(Oid pubid); extern void RemovePublicationRelById(Oid proid); extern void RemovePublicationSchemaById(Oid psoid); +extern void RemoveSchemaPubExceptForRel(Oid relid, Oid oldNspOid, + Oid newNspOid); extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId); extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8a9df884276..d8b6ca132cf 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4510,6 +4510,9 @@ typedef struct PublicationTable Node *whereClause; /* qualifications */ List *columns; /* List of columns in a publication table */ bool except; /* True if listed in the EXCEPT clause */ + bool except_in_schema; /* True if listed in the EXCEPT clause of a + * TABLES IN SCHEMA clause, whose scope is + * limited to that schema */ } PublicationTable; /* @@ -4531,6 +4534,8 @@ typedef struct PublicationObjSpec PublicationObjSpecType pubobjtype; /* type of this publication object */ char *name; PublicationTable *pubtable; + List *except_tables; /* tables specified in the EXCEPT clause (for + * TABLES IN SCHEMA) */ ParseLoc location; /* token location, or -1 if unknown */ } PublicationObjSpec; diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 4f21462cc17..6befa549ae6 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -279,6 +279,12 @@ CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (test ERROR: syntax error at or near "testpub_tbl1" LINE 1: ..._foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tb... ^ +-- fail - EXCEPT is not allowed for FOR TABLE publications +CREATE PUBLICATION testpub_except_err + FOR TABLE testpub_tbl1, testpub_tbl2 EXCEPT (TABLE testpub_tbl3); +ERROR: EXCEPT is not allowed for TABLE publication objects +LINE 2: FOR TABLE testpub_tbl1, testpub_tbl2 EXCEPT (TABLE testp... + ^ --------------------------------------------- -- SET ALL TABLES/SEQUENCES --------------------------------------------- @@ -473,13 +479,292 @@ CREATE TABLE tab_main (a int) PARTITION BY RANGE(a); -- Attaching a partition is not allowed if the partitioned table appears in a -- publication's EXCEPT clause. ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200); -ERROR: cannot attach table "testpub_root" as partition because it is referenced in publication "testpub8" EXCEPT clause +ERROR: cannot attach table "testpub_root" as partition because it is named in EXCEPT clause of publication "testpub8" DETAIL: The publication EXCEPT clause cannot contain tables that are partitions. -HINT: Change the publication's EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES. +HINT: Change the EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES ... EXCEPT. RESET client_min_messages; DROP TABLE testpub_root, testpub_part1, tab_main; DROP PUBLICATION testpub8; ---- Tests for publications with SEQUENCES +--------------------------------------------- +-- EXCEPT tests for TABLES IN SCHEMA +--------------------------------------------- +SET client_min_messages = 'ERROR'; +-- Create tables in pub_test for these tests +CREATE TABLE pub_test.testpub_tbl_s1 (a int primary key, b text); +CREATE TABLE pub_test.testpub_tbl_s2 (x int primary key, y text); +-- Create same-named tables in public to verify unqualified EXCEPT entries +-- are qualified with the named schema, not public +CREATE TABLE testpub_nopk (foo int, bar int); +CREATE TABLE testpub_tbl_s1 (a int primary key, b text); +-- Basic: exclude one table from a schema publication +CREATE PUBLICATION testpub_schema_except1 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +\dRp+ testpub_schema_except1 + Publication testpub_schema_except1 + 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" + +-- Exclude multiple tables using unqualified names; same-named tables exist in +-- public to confirm unqualified names resolve to pub_test, not public +CREATE PUBLICATION testpub_schema_except2 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_nopk, testpub_tbl_s1); +\dRp+ testpub_schema_except2 + Publication testpub_schema_except2 + 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_nopk" + "pub_test.testpub_tbl_s1" + +-- fail: EXCEPT table belongs to a different schema +CREATE PUBLICATION testpub_except_wrongschema + FOR 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 2: FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE public.testp... + ^ +-- fail: cross-schema EXCEPT not allowed; each EXCEPT is bound to its immediate schema +CREATE PUBLICATION testpub_except_crossschema + FOR TABLES IN SCHEMA pub_test, public EXCEPT (TABLE pub_test.testpub_tbl_s1, public.testpub_tbl1); +ERROR: table "pub_test.testpub_tbl_s1" in EXCEPT clause does not belong to schema "public" +LINE 2: ...R TABLES IN SCHEMA pub_test, public EXCEPT (TABLE pub_test.t... + ^ +-- Multiple schemas each with their own EXCEPT clause +CREATE PUBLICATION testpub_schema_except_multi + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1), + public EXCEPT (TABLE testpub_tbl1); +\dRp+ testpub_schema_except_multi + Publication testpub_schema_except_multi + 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" + "public" +Except tables: + "pub_test.testpub_tbl_s1" + "public.testpub_tbl1" + +-- fail: same schema repeated with same EXCEPT clauses +CREATE PUBLICATION testpub_schema_except_conflict + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1), + pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +ERROR: conflicting or redundant EXCEPT clauses for schema "pub_test" +-- fail: same schema repeated with conflicting or no EXCEPT clauses +CREATE PUBLICATION testpub_schema_except_conflict2 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1), + pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2), + pub_test; +ERROR: conflicting or redundant EXCEPT clauses for schema "pub_test" +-- ALTER TABLE ... SET SCHEMA on a table excluded by a schema publication: +-- the schema-scoped exclusion is no longer meaningful once the table moves +-- out of its schema, so the exclusion is auto-removed. +CREATE PUBLICATION testpub_schema_except_setsch + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2); +\dRp+ testpub_schema_except_setsch + Publication testpub_schema_except_setsch + 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" + +ALTER TABLE pub_test.testpub_tbl_s2 SET SCHEMA public; +\dRp+ testpub_schema_except_setsch + Publication testpub_schema_except_setsch + 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" + +DROP PUBLICATION testpub_schema_except_setsch; +-- Restore for further tests +ALTER TABLE public.testpub_tbl_s2 SET SCHEMA pub_test; +-- fail: table appears in both the explicit table list and the EXCEPT clause +CREATE PUBLICATION testpub_except_conflict + FOR 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: nonexistent table in EXCEPT clause +CREATE PUBLICATION testpub_except_norel + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table); +ERROR: relation "pub_test.nonexistent_table" does not exist +-- fail: partition cannot appear in EXCEPT clause; only root tables are allowed +CREATE TABLE pub_test.testpub_parted_s (a int) PARTITION BY LIST (a); +CREATE TABLE pub_test.testpub_part_s PARTITION OF pub_test.testpub_parted_s FOR VALUES IN (1); +CREATE PUBLICATION testpub_except_partition + FOR 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. +-- fail: TABLE keyword is required for the first entry in the EXCEPT clause +CREATE PUBLICATION testpub_except_nokw + FOR TABLES IN SCHEMA pub_test EXCEPT (testpub_nopk); +ERROR: syntax error at or near "testpub_nopk" +LINE 2: FOR TABLES IN SCHEMA pub_test EXCEPT (testpub_nopk); + ^ +-- Check that a table excluded from a schema publication is reported only as +-- excluded and not as published. +\d pub_test.testpub_nopk + Table "pub_test.testpub_nopk" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + foo | integer | | | + bar | integer | | | +Included in publications: + "testpub_schema_except1" + "testpub_schema_except_multi" +Excluded from publications: + "testpub_schema_except2" + +-- fail: ADD TABLE for a partition is rejected when a partition ancestor +-- is currently in the publication's EXCEPT list. +CREATE PUBLICATION testpub_except_ancestor + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); +ALTER PUBLICATION testpub_except_ancestor ADD TABLE pub_test.testpub_part_s; +ERROR: cannot add partition "pub_test.testpub_part_s" to publication "testpub_except_ancestor" +DETAIL: Partition root "pub_test.testpub_parted_s" is named in the publication's EXCEPT clause for schema "pub_test". +HINT: Change the EXCEPT clause using ALTER PUBLICATION ... SET TABLES IN SCHEMA ... EXCEPT. +DROP PUBLICATION testpub_except_ancestor; +-- fail: same contradiction (explicit partition + EXCEPT-ed ancestor), but +-- given in a single statement instead of two separate ones +CREATE PUBLICATION testpub_except_ancestor_same_create + FOR 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: same contradiction as above, but the partition lives in a different +-- schema (public) than its EXCEPT-ed root (pub_test) +CREATE TABLE public.testpub_part_s2 PARTITION OF pub_test.testpub_parted_s FOR VALUES IN (2); +CREATE PUBLICATION testpub_except_ancestor_cross_schema + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s), + TABLE public.testpub_part_s2; +ERROR: partition "public.testpub_part_s2" 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". +DROP TABLE public.testpub_part_s2; +-- ALTER PUBLICATION ... SET replaces the publication's whole relation list, so +-- an existing entry for a relation now requested with the opposite kind must be +-- dropped and re-added, not silently left in place. +CREATE PUBLICATION testpub_except_alter1 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +-- the excluded table becomes an ordinary member: one non-EXCEPT entry, no +-- EXCEPT entry, and no schema left in the publication +ALTER PUBLICATION testpub_except_alter1 SET TABLE pub_test.testpub_tbl_s1; +\dRp+ testpub_except_alter1 + Publication testpub_except_alter1 + 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: + "pub_test.testpub_tbl_s1" + +DROP PUBLICATION testpub_except_alter1; +-- and the reverse direction: an ordinary member becomes an EXCEPT entry +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); +\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: + "pub_test.testpub_tbl_s1" + +DROP PUBLICATION testpub_except_alter2; +-- Cross-schema inheritance children and EXCEPT. An EXCEPT clause also +-- excludes inheritance children of a parent named without ONLY. +CREATE TABLE pub_test.testpub_inh_parent (a int); +CREATE TABLE pub_test.testpub_inh_sibling (b int) INHERITS (pub_test.testpub_inh_parent); +CREATE TABLE testpub_inh_child (b int) INHERITS (pub_test.testpub_inh_parent); +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent); +-- Two EXCEPT entries: the parent and the child in pub_test. +\dRp+ testpub_inh + Publication testpub_inh + 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_inh_parent" + "pub_test.testpub_inh_sibling" + +DROP PUBLICATION testpub_inh; +-- fail: the child testpub_inh_child is also included through TABLES IN SCHEM +-- public +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent), + 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". +-- 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), + TABLE testpub_inh_child; +ERROR: table "public.testpub_inh_child" cannot be both published and excluded +DETAIL: It inherits from a table named in an EXCEPT clause of this statement. +HINT: Use ONLY in the EXCEPT clause to exclude just the parent, or do not publish this table explicitly. +-- ONLY resolves the conflict: only the parent is excluded, so the child can +-- be published explicitly +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE ONLY pub_test.testpub_inh_parent), + TABLE testpub_inh_child; +\dRp+ testpub_inh + Publication testpub_inh + 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: + "public.testpub_inh_child" +Tables from schemas: + "pub_test" +Except tables: + "pub_test.testpub_inh_parent" + +DROP PUBLICATION testpub_inh; +-- An EXCEPT clause in the child's schema also excludes the child explicitly, +-- so both clauses agree and there is no conflict. +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent), + TABLES IN SCHEMA public EXCEPT (TABLE testpub_inh_child); +\dRp+ testpub_inh + Publication testpub_inh + 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" + "public" +Except tables: + "pub_test.testpub_inh_parent" + "pub_test.testpub_inh_sibling" + "public.testpub_inh_child" + +DROP PUBLICATION testpub_inh; +DROP TABLE testpub_inh_child, pub_test.testpub_inh_sibling, + pub_test.testpub_inh_parent; +-- 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; +--------------------------------------------- +-- Tests for publications with SEQUENCES +--------------------------------------------- CREATE SEQUENCE regress_pub_seq0; CREATE SEQUENCE pub_test.regress_pub_seq1; -- FOR ALL SEQUENCES @@ -1970,6 +2255,54 @@ ALTER PUBLICATION testpub1_forschema ADD TABLES IN SCHEMA foo, bar (a, b); ERROR: column specification not allowed for schema LINE 1: ...TION testpub1_forschema ADD TABLES IN SCHEMA foo, bar (a, b)... ^ +-- EXCEPT clause with CURRENT_SCHEMA: cross-schema entry must be rejected +SET search_path = pub_test1; +-- qualified name from wrong schema -> error +CREATE PUBLICATION testpub_cursch_except FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE pub_test2.tbl1); +ERROR: table "pub_test2.tbl1" in EXCEPT clause does not belong to schema "pub_test1" +LINE 1: ...FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE pub_test2.... + ^ +-- unqualified name implicitly qualified with current schema (pub_test1.tbl) +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub_cursch_except FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE tbl); +RESET client_min_messages; +\dRp+ testpub_cursch_except + Publication testpub_cursch_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_test1" +Except tables: + "pub_test1.tbl" + +DROP PUBLICATION testpub_cursch_except; +-- succeeds: CURRENT_SCHEMA and pub_test1 (same schema) +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub_cursch_named_same + FOR TABLES IN SCHEMA CURRENT_SCHEMA, pub_test1; +RESET client_min_messages; +\dRp+ testpub_cursch_named_same + Publication testpub_cursch_named_same + 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_test1" + +DROP PUBLICATION testpub_cursch_named_same; +-- fail: CURRENT_SCHEMA and pub_test1 (same schema) have +-- conflicting EXCEPT clauses +CREATE PUBLICATION testpub_cursch_named_conflict + FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE tbl), + pub_test1 EXCEPT (TABLE tbl1); +ERROR: conflicting or redundant EXCEPT clauses for schema "pub_test1" +-- fail: two CURRENT_SCHEMA mentions with conflicting EXCEPT clauses +CREATE PUBLICATION testpub_cursch_cursch_conflict + FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE tbl), + CURRENT_SCHEMA EXCEPT (TABLE tbl1); +ERROR: conflicting or redundant EXCEPT clauses for schema "pub_test1" +RESET search_path; -- cleanup pub_test1 schema for invalidation tests ALTER PUBLICATION testpub2_forschema DROP TABLES IN SCHEMA pub_test1; DROP PUBLICATION testpub3_forschema, testpub4_forschema, testpub5_forschema, testpub6_forschema, testpub_fortable; @@ -2321,6 +2654,7 @@ DROP ROLE regress_publication_user_dummy; -- Test pg_get_publication_tables(text[], oid) function CREATE SCHEMA gpt_test_sch; CREATE TABLE gpt_test_sch.tbl_sch (id int); +CREATE TABLE gpt_test_sch.tbl_sch2 (id int); CREATE TABLE tbl_normal (id int); CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1); CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10); @@ -2331,6 +2665,7 @@ CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true); CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false); CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch; +CREATE PUBLICATION pub_schema_except FOR TABLES IN SCHEMA gpt_test_sch EXCEPT (TABLE gpt_test_sch.tbl_sch); CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10); CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false); CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true); @@ -2482,6 +2817,44 @@ SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no r ---------+---------+-------+------ (0 rows) +-- test for EXCEPT clause with schema publication +SELECT * FROM test_gpt(ARRAY['pub_schema_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_schema_except'], 'gpt_test_sch.tbl_sch2'); -- one row (included via schema) + pubname | relname | attrs | qual +-------------------+----------+-------+------ + pub_schema_except | tbl_sch2 | 1 | +(1 row) + +-- test for EXCEPT clause with schema publication, where the excluded root's +-- partition lives in a different schema that is separately, fully included +-- (no EXCEPT) in the same publication. The root's exclusion must cascade to +-- the partition regardless of the partition's own schema membership. +CREATE SCHEMA gpt_cross_sch1; +CREATE SCHEMA gpt_cross_sch2; +CREATE TABLE gpt_cross_sch1.croot (id int) PARTITION BY RANGE (id); +CREATE TABLE gpt_cross_sch2.cpart1 PARTITION OF gpt_cross_sch1.croot FOR VALUES FROM (1) TO (10); +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub_cross_schema_except FOR TABLES IN SCHEMA gpt_cross_sch1 EXCEPT (TABLE gpt_cross_sch1.croot), TABLES IN SCHEMA gpt_cross_sch2; +RESET client_min_messages; +SELECT * FROM test_gpt(ARRAY['pub_cross_schema_except'], 'gpt_cross_sch1.croot'); -- no result (excluded) + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- no result (excluded via cascading root exclusion, even though cpart1's own +-- schema gpt_cross_sch2 is separately, fully included with no EXCEPT) +SELECT * FROM test_gpt(ARRAY['pub_cross_schema_except'], 'gpt_cross_sch2.cpart1'); + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +DROP PUBLICATION pub_cross_schema_except; +DROP TABLE gpt_cross_sch2.cpart1, gpt_cross_sch1.croot; +DROP SCHEMA gpt_cross_sch1, gpt_cross_sch2; -- two rows with different row filter SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal'); pubname | relname | attrs | qual @@ -2534,6 +2907,7 @@ DROP PUBLICATION pub_all_no_viaroot; DROP PUBLICATION pub_all_except; DROP PUBLICATION pub_all_except_no_viaroot; DROP PUBLICATION pub_schema; +DROP PUBLICATION pub_schema_except; DROP PUBLICATION pub_normal; DROP PUBLICATION pub_part_leaf; DROP PUBLICATION pub_part_parent; @@ -2542,7 +2916,9 @@ DROP PUBLICATION pub_part_parent_child; DROP VIEW gpt_test_view; DROP TABLE tbl_normal, tbl_parent, tbl_part1; DROP SCHEMA gpt_test_sch CASCADE; -NOTICE: drop cascades to table gpt_test_sch.tbl_sch +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table gpt_test_sch.tbl_sch +drop cascades to table gpt_test_sch.tbl_sch2 -- stage objects for pg_dump tests CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int); CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int); diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index fac54b02e27..f77dba51349 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -125,6 +125,9 @@ CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (TABL \d testpub_tbl1 -- fail - first table in the EXCEPT list should use TABLE keyword CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); +-- fail - EXCEPT is not allowed for FOR TABLE publications +CREATE PUBLICATION testpub_except_err + FOR TABLE testpub_tbl1, testpub_tbl2 EXCEPT (TABLE testpub_tbl3); --------------------------------------------- -- SET ALL TABLES/SEQUENCES @@ -222,7 +225,177 @@ RESET client_min_messages; DROP TABLE testpub_root, testpub_part1, tab_main; DROP PUBLICATION testpub8; ---- Tests for publications with SEQUENCES +--------------------------------------------- +-- EXCEPT tests for TABLES IN SCHEMA +--------------------------------------------- +SET client_min_messages = 'ERROR'; +-- Create tables in pub_test for these tests +CREATE TABLE pub_test.testpub_tbl_s1 (a int primary key, b text); +CREATE TABLE pub_test.testpub_tbl_s2 (x int primary key, y text); +-- Create same-named tables in public to verify unqualified EXCEPT entries +-- are qualified with the named schema, not public +CREATE TABLE testpub_nopk (foo int, bar int); +CREATE TABLE testpub_tbl_s1 (a int primary key, b text); + +-- Basic: exclude one table from a schema publication +CREATE PUBLICATION testpub_schema_except1 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +\dRp+ testpub_schema_except1 + +-- Exclude multiple tables using unqualified names; same-named tables exist in +-- public to confirm unqualified names resolve to pub_test, not public +CREATE PUBLICATION testpub_schema_except2 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_nopk, testpub_tbl_s1); +\dRp+ testpub_schema_except2 + +-- fail: EXCEPT table belongs to a different schema +CREATE PUBLICATION testpub_except_wrongschema + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE public.testpub_tbl1); + +-- fail: cross-schema EXCEPT not allowed; each EXCEPT is bound to its immediate schema +CREATE PUBLICATION testpub_except_crossschema + FOR TABLES IN SCHEMA pub_test, public EXCEPT (TABLE pub_test.testpub_tbl_s1, public.testpub_tbl1); + +-- Multiple schemas each with their own EXCEPT clause +CREATE PUBLICATION testpub_schema_except_multi + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1), + public EXCEPT (TABLE testpub_tbl1); +\dRp+ testpub_schema_except_multi + +-- fail: same schema repeated with same EXCEPT clauses +CREATE PUBLICATION testpub_schema_except_conflict + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1), + pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); + +-- fail: same schema repeated with conflicting or no EXCEPT clauses +CREATE PUBLICATION testpub_schema_except_conflict2 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1), + pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2), + pub_test; + +-- ALTER TABLE ... SET SCHEMA on a table excluded by a schema publication: +-- the schema-scoped exclusion is no longer meaningful once the table moves +-- out of its schema, so the exclusion is auto-removed. +CREATE PUBLICATION testpub_schema_except_setsch + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2); +\dRp+ testpub_schema_except_setsch +ALTER TABLE pub_test.testpub_tbl_s2 SET SCHEMA public; +\dRp+ testpub_schema_except_setsch +DROP PUBLICATION testpub_schema_except_setsch; +-- Restore for further tests +ALTER TABLE public.testpub_tbl_s2 SET SCHEMA pub_test; + +-- fail: table appears in both the explicit table list and the EXCEPT clause +CREATE PUBLICATION testpub_except_conflict + FOR TABLE pub_test.testpub_tbl_s1, TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); + +-- fail: nonexistent table in EXCEPT clause +CREATE PUBLICATION testpub_except_norel + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table); + +-- fail: partition cannot appear in EXCEPT clause; only root tables are allowed +CREATE TABLE pub_test.testpub_parted_s (a int) PARTITION BY LIST (a); +CREATE TABLE pub_test.testpub_part_s PARTITION OF pub_test.testpub_parted_s FOR VALUES IN (1); +CREATE PUBLICATION testpub_except_partition + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_part_s); + +-- fail: TABLE keyword is required for the first entry in the EXCEPT clause +CREATE PUBLICATION testpub_except_nokw + FOR TABLES IN SCHEMA pub_test EXCEPT (testpub_nopk); + +-- Check that a table excluded from a schema publication is reported only as +-- excluded and not as published. +\d pub_test.testpub_nopk + +-- fail: ADD TABLE for a partition is rejected when a partition ancestor +-- is currently in the publication's EXCEPT list. +CREATE PUBLICATION testpub_except_ancestor + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); +ALTER PUBLICATION testpub_except_ancestor ADD TABLE pub_test.testpub_part_s; +DROP PUBLICATION testpub_except_ancestor; + +-- fail: same contradiction (explicit partition + EXCEPT-ed ancestor), but +-- given in a single statement instead of two separate ones +CREATE PUBLICATION testpub_except_ancestor_same_create + FOR TABLE pub_test.testpub_part_s, + TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s); + +-- fail: same contradiction as above, but the partition lives in a different +-- schema (public) than its EXCEPT-ed root (pub_test) +CREATE TABLE public.testpub_part_s2 PARTITION OF pub_test.testpub_parted_s FOR VALUES IN (2); +CREATE PUBLICATION testpub_except_ancestor_cross_schema + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_parted_s), + TABLE public.testpub_part_s2; +DROP TABLE public.testpub_part_s2; + +-- ALTER PUBLICATION ... SET replaces the publication's whole relation list, so +-- an existing entry for a relation now requested with the opposite kind must be +-- dropped and re-added, not silently left in place. +CREATE PUBLICATION testpub_except_alter1 + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1); +-- the excluded table becomes an ordinary member: one non-EXCEPT entry, no +-- EXCEPT entry, and no schema left in the publication +ALTER PUBLICATION testpub_except_alter1 SET TABLE pub_test.testpub_tbl_s1; +\dRp+ testpub_except_alter1 +DROP PUBLICATION testpub_except_alter1; + +-- and the reverse direction: an ordinary member becomes an EXCEPT entry +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); +\dRp+ testpub_except_alter2 +DROP PUBLICATION testpub_except_alter2; + +-- Cross-schema inheritance children and EXCEPT. An EXCEPT clause also +-- excludes inheritance children of a parent named without ONLY. +CREATE TABLE pub_test.testpub_inh_parent (a int); +CREATE TABLE pub_test.testpub_inh_sibling (b int) INHERITS (pub_test.testpub_inh_parent); +CREATE TABLE testpub_inh_child (b int) INHERITS (pub_test.testpub_inh_parent); +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent); +-- Two EXCEPT entries: the parent and the child in pub_test. +\dRp+ testpub_inh +DROP PUBLICATION testpub_inh; + +-- fail: the child testpub_inh_child is also included through TABLES IN SCHEM +-- public +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent), + TABLES IN SCHEMA public; + +-- 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), + TABLE testpub_inh_child; + +-- ONLY resolves the conflict: only the parent is excluded, so the child can +-- be published explicitly +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE ONLY pub_test.testpub_inh_parent), + TABLE testpub_inh_child; +\dRp+ testpub_inh +DROP PUBLICATION testpub_inh; + +-- An EXCEPT clause in the child's schema also excludes the child explicitly, +-- so both clauses agree and there is no conflict. +CREATE PUBLICATION testpub_inh + FOR TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_inh_parent), + TABLES IN SCHEMA public EXCEPT (TABLE testpub_inh_child); +\dRp+ testpub_inh +DROP PUBLICATION testpub_inh; +DROP TABLE testpub_inh_child, pub_test.testpub_inh_sibling, + pub_test.testpub_inh_parent; + +-- 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; + +--------------------------------------------- +-- Tests for publications with SEQUENCES +--------------------------------------------- CREATE SEQUENCE regress_pub_seq0; CREATE SEQUENCE pub_test.regress_pub_seq1; @@ -1194,6 +1367,38 @@ ALTER PUBLICATION testpub1_forschema SET TABLES IN SCHEMA pub_test1, pub_test1; ALTER PUBLICATION testpub1_forschema ADD TABLES IN SCHEMA foo (a, b); ALTER PUBLICATION testpub1_forschema ADD TABLES IN SCHEMA foo, bar (a, b); +-- EXCEPT clause with CURRENT_SCHEMA: cross-schema entry must be rejected +SET search_path = pub_test1; +-- qualified name from wrong schema -> error +CREATE PUBLICATION testpub_cursch_except FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE pub_test2.tbl1); +-- unqualified name implicitly qualified with current schema (pub_test1.tbl) +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub_cursch_except FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE tbl); +RESET client_min_messages; +\dRp+ testpub_cursch_except +DROP PUBLICATION testpub_cursch_except; + +-- succeeds: CURRENT_SCHEMA and pub_test1 (same schema) +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub_cursch_named_same + FOR TABLES IN SCHEMA CURRENT_SCHEMA, pub_test1; +RESET client_min_messages; +\dRp+ testpub_cursch_named_same +DROP PUBLICATION testpub_cursch_named_same; + +-- fail: CURRENT_SCHEMA and pub_test1 (same schema) have +-- conflicting EXCEPT clauses +CREATE PUBLICATION testpub_cursch_named_conflict + FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE tbl), + pub_test1 EXCEPT (TABLE tbl1); + +-- fail: two CURRENT_SCHEMA mentions with conflicting EXCEPT clauses +CREATE PUBLICATION testpub_cursch_cursch_conflict + FOR TABLES IN SCHEMA CURRENT_SCHEMA EXCEPT (TABLE tbl), + CURRENT_SCHEMA EXCEPT (TABLE tbl1); + +RESET search_path; + -- cleanup pub_test1 schema for invalidation tests ALTER PUBLICATION testpub2_forschema DROP TABLES IN SCHEMA pub_test1; DROP PUBLICATION testpub3_forschema, testpub4_forschema, testpub5_forschema, testpub6_forschema, testpub_fortable; @@ -1448,6 +1653,7 @@ DROP ROLE regress_publication_user_dummy; -- Test pg_get_publication_tables(text[], oid) function CREATE SCHEMA gpt_test_sch; CREATE TABLE gpt_test_sch.tbl_sch (id int); +CREATE TABLE gpt_test_sch.tbl_sch2 (id int); CREATE TABLE tbl_normal (id int); CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1); CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10); @@ -1459,6 +1665,7 @@ CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true); CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false); CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch; +CREATE PUBLICATION pub_schema_except FOR TABLES IN SCHEMA gpt_test_sch EXCEPT (TABLE gpt_test_sch.tbl_sch); CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10); CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false); CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true); @@ -1515,6 +1722,32 @@ SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded) SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result +-- test for EXCEPT clause with schema publication +SELECT * FROM test_gpt(ARRAY['pub_schema_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_schema_except'], 'gpt_test_sch.tbl_sch2'); -- one row (included via schema) + +-- test for EXCEPT clause with schema publication, where the excluded root's +-- partition lives in a different schema that is separately, fully included +-- (no EXCEPT) in the same publication. The root's exclusion must cascade to +-- the partition regardless of the partition's own schema membership. +CREATE SCHEMA gpt_cross_sch1; +CREATE SCHEMA gpt_cross_sch2; +CREATE TABLE gpt_cross_sch1.croot (id int) PARTITION BY RANGE (id); +CREATE TABLE gpt_cross_sch2.cpart1 PARTITION OF gpt_cross_sch1.croot FOR VALUES FROM (1) TO (10); + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub_cross_schema_except FOR TABLES IN SCHEMA gpt_cross_sch1 EXCEPT (TABLE gpt_cross_sch1.croot), TABLES IN SCHEMA gpt_cross_sch2; +RESET client_min_messages; + +SELECT * FROM test_gpt(ARRAY['pub_cross_schema_except'], 'gpt_cross_sch1.croot'); -- no result (excluded) +-- no result (excluded via cascading root exclusion, even though cpart1's own +-- schema gpt_cross_sch2 is separately, fully included with no EXCEPT) +SELECT * FROM test_gpt(ARRAY['pub_cross_schema_except'], 'gpt_cross_sch2.cpart1'); + +DROP PUBLICATION pub_cross_schema_except; +DROP TABLE gpt_cross_sch2.cpart1, gpt_cross_sch1.croot; +DROP SCHEMA gpt_cross_sch1, gpt_cross_sch2; + -- two rows with different row filter SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal'); @@ -1543,6 +1776,7 @@ DROP PUBLICATION pub_all_no_viaroot; DROP PUBLICATION pub_all_except; DROP PUBLICATION pub_all_except_no_viaroot; DROP PUBLICATION pub_schema; +DROP PUBLICATION pub_schema_except; DROP PUBLICATION pub_normal; DROP PUBLICATION pub_part_leaf; DROP PUBLICATION pub_part_parent; diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl index 43b51c8ff71..7e11061a594 100644 --- a/src/test/subscription/t/037_except.pl +++ b/src/test/subscription/t/037_except.pl @@ -24,14 +24,17 @@ my $result; sub test_except_root_partition { - my ($pubviaroot) = @_; + my ($pubviaroot, $pubsql) = @_; + $pubsql //= + "CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT (TABLE root1)"; + $pubsql .= " WITH (publish_via_partition_root = $pubviaroot)"; # If the root partitioned table is in the EXCEPT clause, all its # partitions are excluded from publication, regardless of the # publish_via_partition_root setting. $node_publisher->safe_psql( 'postgres', qq( - CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT (TABLE root1) WITH (publish_via_partition_root = $pubviaroot); + $pubsql; INSERT INTO root1 VALUES (1), (101); )); $node_subscriber->safe_psql('postgres', @@ -223,6 +226,206 @@ $node_subscriber->safe_psql( test_except_root_partition('false'); test_except_root_partition('true'); +# Same validation using TABLES IN SCHEMA instead of FOR ALL TABLES. +my $schema_pub = + "CREATE PUBLICATION tap_pub_part FOR TABLES IN SCHEMA public EXCEPT (TABLE public.root1)"; +test_except_root_partition('false', $schema_pub); +test_except_root_partition('true', $schema_pub); + +# ============================================ +# EXCEPT test cases for TABLES IN SCHEMA +# ============================================ + +# Create a dedicated schema with two tables: one to be published and one to be +# excluded. Also create inherited tables to verify ONLY semantics. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE SCHEMA sch1; + CREATE TABLE sch1.tab_published AS SELECT generate_series(1,5) AS a; + CREATE TABLE sch1.tab_excluded AS SELECT generate_series(1,5) AS a; + CREATE TABLE sch1.parent (a int); + CREATE TABLE sch1.child (b int) INHERITS (sch1.parent); +)); + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE SCHEMA sch1; + CREATE TABLE sch1.tab_published (a int); + CREATE TABLE sch1.tab_excluded (a int); + CREATE TABLE sch1.parent (a int); + CREATE TABLE sch1.child (b int) INHERITS (sch1.parent); +)); + +# Basic test: initial sync respects EXCEPT. +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION sch_pub FOR 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(5), + 'TABLES IN SCHEMA EXCEPT: initial sync copies included table'); +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_excluded"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: initial sync skips excluded table'); + +# DML: only the included table should be replicated. +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO sch1.tab_published VALUES (6); + INSERT INTO sch1.tab_excluded VALUES (6); +)); +$node_publisher->wait_for_catchup('sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_published"); +is($result, qq(6), + 'TABLES IN SCHEMA EXCEPT: DML on included table is replicated'); +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_excluded"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: DML on excluded table is not replicated'); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sch_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION sch_pub'); + +# Inherited tables: excluding the parent (without ONLY) also excludes the child. +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION sch_pub FOR TABLES IN SCHEMA sch1 EXCEPT (TABLE sch1.parent)" +); +$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'); + +$node_publisher->safe_psql('postgres', + "INSERT INTO sch1.child VALUES (generate_series(1,5), generate_series(1,5))" +); +$node_publisher->wait_for_catchup('sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM sch1.child"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: excluding parent (without ONLY) also excludes child' +); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sch_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION sch_pub'); + +# Test that EXCEPT (TABLE ONLY parent) excludes only the parent itself, not its +# child. Truncate child first so rows from the previous test are not copied by +# the initial table sync of the next subscription. +$node_publisher->safe_psql('postgres', 'TRUNCATE sch1.child'); +$node_subscriber->safe_psql('postgres', 'TRUNCATE sch1.child'); +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION sch_pub FOR TABLES IN SCHEMA sch1 EXCEPT (TABLE ONLY sch1.parent)" +); +$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'); + +$node_publisher->safe_psql('postgres', + "INSERT INTO sch1.child VALUES (generate_series(1,5), generate_series(1,5))" +); +$node_publisher->wait_for_catchup('sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM sch1.child"); +is($result, qq(5), + 'TABLES IN SCHEMA EXCEPT: ONLY parent in EXCEPT does not exclude child'); + +$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'); + +# ============================================ +# EXCEPT test cases for TABLES IN SCHEMA with a cross-schema partition +# ============================================ + +# A partition can live in a different schema than its partitioned root. If +# the root is excluded via EXCEPT under its own schema's clause, the +# exclusion must cascade to all its partitions, even when the partition's +# own schema is separately, fully included (no EXCEPT) in the same +# publication. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE SCHEMA csch1; + CREATE SCHEMA csch2; + CREATE TABLE csch1.croot(a int) PARTITION BY RANGE(a); + CREATE TABLE csch2.cpart1 PARTITION OF csch1.croot FOR VALUES FROM (0) TO (100); + INSERT INTO csch1.croot VALUES (generate_series(1,5)); +)); + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE SCHEMA csch1; + CREATE SCHEMA csch2; + CREATE TABLE csch1.croot(a int); + CREATE TABLE csch2.cpart1(a int); +)); + +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION cross_sch_pub FOR TABLES IN SCHEMA csch1 EXCEPT (TABLE csch1.croot), TABLES IN SCHEMA csch2 WITH (publish_via_partition_root = false)" +); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION cross_sch_sub CONNECTION '$publisher_connstr' PUBLICATION cross_sch_pub" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'cross_sch_sub'); + +# Baseline: the root itself, pre-populated with rows before the subscription +# was created, is excluded by its own schema's EXCEPT clause -- initial sync +# must not have copied it. +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM csch1.croot"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: cross-schema partition root is excluded by initial sync' +); + +# Regression case: the partition's own schema (csch2) is separately, fully +# included with no EXCEPT, but the root's exclusion must still cascade to it +# -- initial sync must not have copied the pre-existing rows routed into this +# partition via the root. +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM csch2.cpart1"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: cross-schema partition is excluded by initial sync via cascading root exclusion' +); + +# Insert distinct, identifiable data directly into the partition and verify +# it is not replicated either. +$node_publisher->safe_psql('postgres', + "INSERT INTO csch2.cpart1 VALUES (generate_series(1,5))"); +$node_publisher->wait_for_catchup('cross_sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM csch1.croot"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: cross-schema partition root remains excluded after DML' +); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM csch2.cpart1"); +is($result, qq(0), + 'TABLES IN SCHEMA EXCEPT: cross-schema partition remains excluded after DML via cascading root exclusion' +); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION cross_sch_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION cross_sch_pub'); +$node_publisher->safe_psql('postgres', 'DROP SCHEMA csch1, csch2 CASCADE'); +$node_subscriber->safe_psql('postgres', 'DROP SCHEMA csch1, csch2 CASCADE'); + # ============================================ # Test when a subscription is subscribing to multiple publications # ============================================ @@ -254,6 +457,7 @@ $node_publisher->safe_psql( DROP PUBLICATION tap_pub2; TRUNCATE tab1; )); +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub'); $node_subscriber->safe_psql('postgres', qq(TRUNCATE tab1)); # OK when a table is excluded by pub1 EXCEPT clause, but it is included by pub2 @@ -282,6 +486,59 @@ $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 the EXCEPT clause of one schema publication, +# but it is included by another publication that publishes the same schema in +# full. Inclusion wins, so the table is replicated. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE SCHEMA msch; + CREATE TABLE msch.tab_excl (a int); + INSERT INTO msch.tab_excl VALUES (1); + CREATE PUBLICATION tap_pub_sch_with_except + FOR TABLES IN SCHEMA msch EXCEPT (TABLE msch.tab_excl); + CREATE PUBLICATION tap_pub_sch_without_except FOR TABLES IN SCHEMA msch; +)); + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE SCHEMA msch; + CREATE TABLE msch.tab_excl (a int); +)); + +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub_multi CONNECTION '$publisher_connstr' PUBLICATION tap_pub_sch_with_except, tap_pub_sch_without_except" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, + 'tap_sub_multi'); + +# The initial table sync must copy the excluded table too, because +# tap_pub_sch_without_except publishes it. +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM msch.tab_excl"); +is($result, qq(1), + 'initial sync copies a table excluded by one publication but included by another' +); + +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO msch.tab_excl VALUES (2); +)); +$node_publisher->wait_for_catchup('tap_sub_multi'); + +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM msch.tab_excl"); +is($result, qq(2), + 'DML on a table excluded by one publication but included by another is replicated' +); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub_multi'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub_sch_with_except'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub_sch_without_except'); +$node_publisher->safe_psql('postgres', 'DROP SCHEMA msch CASCADE'); +$node_subscriber->safe_psql('postgres', 'DROP SCHEMA msch CASCADE'); + $node_publisher->stop('fast'); done_testing(); -- 2.55.0