From a0e5756206d264f7f675755cbd25065cffce1f7a Mon Sep 17 00:00:00 2001 From: Nisha Moond Date: Wed, 15 Jul 2026 21:18:51 +0530 Subject: [PATCH v22 2/5] Restrict conflicting EXCEPT lists in multi-schema publications Add ProcessSchemaExceptTables(), which errors if the same schema is mentioned more than once with EXCEPT, or both with and without EXCEPT. It also checks that a schema's EXCEPT clause only lists tables from that same schema. --- src/backend/commands/publicationcmds.c | 117 +++++++++++++++------- src/backend/parser/gram.y | 37 +------ src/test/regress/expected/publication.out | 38 +++++++ src/test/regress/sql/publication.sql | 31 ++++++ 4 files changed, 155 insertions(+), 68 deletions(-) diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index ba35e48b306..780374ef7f4 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -73,6 +73,10 @@ static void PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists, static void PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok); static char defGetGeneratedColsOption(DefElem *def); static void CheckExceptNotInTableList(List *except_rels, List *explicitrelids); +static void ProcessSchemaExceptTables(Oid schemaid, List *except_tables, + ParseState *pstate, List **schemas, + List **schemas_with_except, + List **except_pubtables); static void @@ -177,6 +181,74 @@ parse_publication_options(ParseState *pstate, } } +/* + * Qualify unqualified EXCEPT table names with the given schema (rejecting + * any explicitly qualified with a different schema), and append them to + * *except_pubtables. + * + * Also rejects a schema being mentioned more than once with an EXCEPT + * clause, even if the EXCEPT lists 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. + * + * schemaid: OID of the schema for this TABLES IN SCHEMA mention. + * except_tables: the EXCEPT list (or NIL) attached to this mention. + * *schemas: accumulates all schema OIDs seen so far in this statement. + * *schemas_with_except: tracks which schema OIDs have already been seen + * with an EXCEPT clause in this statement. + * *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 lists 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)); + + *except_pubtables = lappend(*except_pubtables, eobj->pubtable); + } +} + /* * Convert the PublicationObjSpecType list into schema oid list and * PublicationTable list. @@ -187,6 +259,7 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, { ListCell *cell; PublicationObjSpec *pubobj; + List *schemas_with_except = NIL; if (!pubobjspec_list) return; @@ -211,8 +284,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); @@ -224,40 +299,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); - - /* - * Qualify unqualified EXCEPT table names with the resolved - * current schema and reject any explicitly cross-schema - * entries. This mirrors the parse-time handling done for - * TABLES_IN_SCHEMA in preprocess_pubobj_list(), deferred here - * because CURRENT_SCHEMA is not known until execution time. - */ - if (pubobj->except_tables != NIL) - { - char *cur_schema_name = get_namespace_name(schemaid); - - foreach_ptr(PublicationObjSpec, eobj, pubobj->except_tables) - { - const char *eobj_schemaname = - eobj->pubtable->relation->schemaname; - const char *eobj_relname = - eobj->pubtable->relation->relname; - - if (eobj_schemaname == NULL) - eobj->pubtable->relation->schemaname = cur_schema_name; - else if (strcmp(eobj_schemaname, cur_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(eobj_schemaname, eobj_relname), - cur_schema_name)); - - *except_pubtables = lappend(*except_pubtables, - eobj->pubtable); - } - } + ProcessSchemaExceptTables(schemaid, pubobj->except_tables, + pstate, schemas, + &schemas_with_except, + except_pubtables); break; default: /* shouldn't happen */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 18826a74b77..14520a5ccab 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -20872,8 +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. - * Also flattens except_tables from TABLES IN SCHEMA nodes into the list so - * that ObjectsInPublicationToOids() sees them as top-level EXCEPT_TABLE entries. + * + * 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) @@ -20957,36 +20960,6 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner) errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid schema name"), parser_errposition(pubobj->location)); - - /* - * For TABLES_IN_SCHEMA, qualify unqualified EXCEPT table names - * with the parent schema and reject cross-schema entries at parse - * time, then flatten into the top-level list. - * - * For TABLES_IN_CUR_SCHEMA the schema name is not yet known, so - * skip both steps here; ObjectsInPublicationToOids() will - * qualify names and validate schema membership at execution time. - */ - if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA) - { - foreach_ptr(PublicationObjSpec, eobj, pubobj->except_tables) - { - const char *eobj_schemaname = eobj->pubtable->relation->schemaname; - const char *eobj_relname = eobj->pubtable->relation->relname; - - if (eobj_schemaname == NULL) - eobj->pubtable->relation->schemaname = pubobj->name; - else if (strcmp(eobj_schemaname, pubobj->name) != 0) - ereport(ERROR, - errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("table \"%s\" in EXCEPT clause does not belong to schema \"%s\"", - quote_qualified_identifier(eobj_schemaname, eobj_relname), - pubobj->name), - parser_errposition(eobj->location)); - } - pubobjspec_list = list_concat(pubobjspec_list, pubobj->except_tables); - pubobj->except_tables = NIL; - } } prevobjtype = pubobj->pubobjtype; diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index e51ea53466f..3a2db8642cc 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -552,6 +552,17 @@ Except tables: "pub_test.testpub_tbl_s1" "public.testpub_tbl1" +-- fail: same schema repeated with same EXCEPT lists +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 lists for schema "pub_test" +-- fail: same schema repeated with conflicting or no EXCEPT lists +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 lists 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. @@ -2129,6 +2140,8 @@ 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); @@ -2144,6 +2157,31 @@ 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 lists +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 lists for schema "pub_test1" +-- fail: two CURRENT_SCHEMA mentions with conflicting EXCEPT lists +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 lists for schema "pub_test1" RESET search_path; -- cleanup pub_test1 schema for invalidation tests ALTER PUBLICATION testpub2_forschema DROP TABLES IN SCHEMA pub_test1; diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 4a86d699e84..4a79055c4f3 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -262,6 +262,17 @@ CREATE PUBLICATION testpub_schema_except_multi public EXCEPT (TABLE testpub_tbl1); \dRp+ testpub_schema_except_multi +-- fail: same schema repeated with same EXCEPT lists +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 lists +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. @@ -1304,6 +1315,26 @@ CREATE PUBLICATION testpub_cursch_except FOR TABLES IN SCHEMA CURRENT_SCHEMA EXC 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 lists +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 lists +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 -- 2.50.1 (Apple Git-155)