diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index e7067c84ece..69588937719 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -6572,6 +6572,16 @@ SCRAM-SHA-256$<iteration count>:&l + + + prexcept bool + + + True if the table is excluded from the publication. See + EXCEPT TABLE. + + + prqual pg_node_tree diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 5028fe9af09..bcb473c078b 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -116,7 +116,11 @@ FOR TABLES IN SCHEMA, FOR ALL TABLES, or FOR ALL SEQUENCES. Unlike tables, sequences can be synchronized at any time. For more information, see - . + . When a publication is + created with FOR ALL TABLES, a table or set of tables can + be explicitly excluded from publication using the + EXCEPT TABLE + clause. diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml index 6efbb915cec..77066ef680b 100644 --- a/doc/src/sgml/ref/create_publication.sgml +++ b/doc/src/sgml/ref/create_publication.sgml @@ -32,12 +32,16 @@ CREATE PUBLICATION name and publication_all_object is one of: - ALL TABLES + ALL TABLES [ EXCEPT TABLE ( except_table_object [, ... ] ) ] ALL SEQUENCES and table_and_columns is: [ ONLY ] table_name [ * ] [ ( column_name [, ... ] ) ] [ WHERE ( expression ) ] + +and except_table_object is: + + [ ONLY ] table_name [ * ] @@ -164,7 +168,8 @@ CREATE PUBLICATION name Marks the publication as one that replicates changes for all tables in - the database, including tables created in the future. + the database, including tables created in the future. Tables listed in + EXCEPT TABLE are excluded from the publication. @@ -184,6 +189,37 @@ CREATE PUBLICATION name + + EXCEPT TABLE + + + This clause specifies a list of tables to be excluded from the + publication. + + + For inherited tables, if ONLY is specified before the + table name, only that table is excluded from the publication. If + ONLY is not specified, the table and all its descendant + tables (if any) are excluded. Optionally, * can be + specified after the table name to explicitly indicate that descendant + tables are excluded. + + + For partitioned tables, only the root partitioned table may be specified + in EXCEPT TABLE. Doing so excludes the root table and + all of its partitions from replication. The optional + ONLY and * has no effect for + partitioned tables. + + + There can be a case where a subscription includes multiple publications. + In such a case, a table or partition that is included in one publication + and listed in the EXCEPT TABLE clause of another is + considered included for replication. + + + + WITH ( publication_parameter [= value] [, ... ] ) @@ -489,6 +525,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES; all sequences for synchronization: CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES; + + + + + Create a publication that publishes all changes in all tables except + users and departments: + +CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT TABLE (users, departments); + + + + + Create a publication that publishes all sequences for synchronization, and + all changes in all tables except users and + departments: + +CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT TABLE (users, departments); diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 3550bda4d80..0e543fdd6a2 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1 listed. If x is appended to the command name, the results are displayed in expanded mode. - If + is appended to the command name, the tables and - schemas associated with each publication are shown as well. + If + is appended to the command name, the tables, + excluded tables, and schemas associated with each publication are shown + as well. diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 9a4791c573e..aadc7c202c6 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -53,37 +53,48 @@ typedef struct * error if not. */ static void -check_publication_add_relation(Relation targetrel) +check_publication_add_relation(PublicationRelInfo *pri) { + Relation targetrel = pri->relation; + const char *errormsg; + + if (pri->except) + errormsg = gettext_noop("cannot use publication EXCEPT clause for relation \"%s\""); + else + 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, RelationGetRelationName(targetrel)), + errdetail("This operation is not supported for individual partitions."))); + /* Must be a regular or partitioned table */ if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION && RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind))); /* Can't be system table */ if (IsCatalogRelation(targetrel)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail("This operation is not supported for system tables."))); /* UNLOGGED and TEMP relations cannot be part of publication. */ if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail("This operation is not supported for temporary tables."))); else if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail("This operation is not supported for unlogged tables."))); } @@ -366,7 +377,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level foreach(lc, ancestors) { Oid ancestor = lfirst_oid(lc); - List *apubids = GetRelationPublications(ancestor); + List *apubids = GetRelationIncludedPublications(ancestor); List *aschemaPubids = NIL; level++; @@ -466,7 +477,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, RelationGetRelationName(targetrel), pub->name))); } - check_publication_add_relation(targetrel); + check_publication_add_relation(pri); /* Validate and translate column names into a Bitmapset of attnums. */ attnums = pub_collist_validate(pri->relation, pri->columns); @@ -482,6 +493,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, ObjectIdGetDatum(pubid); values[Anum_pg_publication_rel_prrelid - 1] = ObjectIdGetDatum(relid); + values[Anum_pg_publication_rel_prexcept - 1] = + BoolGetDatum(pri->except); /* Add qualifications, if available */ if (pri->whereClause != NULL) @@ -530,17 +543,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, table_close(rel, RowExclusiveLock); /* - * Invalidate relcache so that publication info is rebuilt. - * - * For the partitioned tables, we must invalidate all partitions contained - * in the respective partition hierarchies, not just the one explicitly - * mentioned in the publication. This is required because we implicitly - * publish the child tables when the parent table is published. + * Relations excluded via the EXCEPT clause do not need explicit + * invalidation as CreatePublication() function invalidates all relations + * as part of defining a FOR ALL TABLES publication. */ - relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL, - relid); + if (!pri->except) + { + /* + * Invalidate relcache so that publication info is rebuilt. + * + * For the partitioned tables, we must invalidate all partitions + * contained in the respective partition hierarchies, not just the one + * explicitly mentioned in the publication. This is required because + * we implicitly publish the child tables when the parent table is + * published. + */ + relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL, + relid); - InvalidatePublicationRels(relids); + InvalidatePublicationRels(relids); + } return myself; } @@ -749,23 +771,30 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists) return myself; } -/* Gets list of publication oids for a relation */ -List * -GetRelationPublications(Oid relid) +/* + * Internal function to get the list of publication oids for a relation. + * + * If except_flag is true, returns the list of publication that specified the + * relation in EXCEPT clause; otherwise, returns the list of publications + * in which relation is included. + */ +static List * +get_relation_publications(Oid relid, bool except_flag) { List *result = NIL; CatCList *pubrellist; - int i; /* Find all publications associated with the relation. */ pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP, ObjectIdGetDatum(relid)); - for (i = 0; i < pubrellist->n_members; i++) + for (int i = 0; i < pubrellist->n_members; i++) { HeapTuple tup = &pubrellist->members[i]->tuple; - Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid; + Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); + Oid pubid = pubrel->prpubid; - result = lappend_oid(result, pubid); + if (pubrel->prexcept == except_flag) + result = lappend_oid(result, pubid); } ReleaseSysCacheList(pubrellist); @@ -774,13 +803,33 @@ GetRelationPublications(Oid relid) } /* - * Gets list of relation oids for a publication. - * - * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES - * should use GetAllPublicationRelations(). + * Gets list of publication oids for a relation. */ List * -GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) +GetRelationIncludedPublications(Oid relid) +{ + return get_relation_publications(relid, false); +} + +/* + * Gets list of publication oids which has relation in EXCEPT clause. + */ +List * +GetRelationExcludedPublications(Oid relid) +{ + return get_relation_publications(relid, true); +} + +/* + * Internal function to get the list of relation oids for a publication. + * + * If except_flag is true, returns the list of relations specified in the + * EXCEPT clause of the publication; otherwise, returns the list of relations + * included in the publication. + */ +static List * +get_publication_relations(Oid pubid, PublicationPartOpt pub_partopt, + bool except_flag) { List *result; Relation pubrelsrel; @@ -805,8 +854,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) Form_pg_publication_rel pubrel; pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); - result = GetPubPartitionOptionRelations(result, pub_partopt, - pubrel->prrelid); + + if (except_flag == pubrel->prexcept) + result = GetPubPartitionOptionRelations(result, pub_partopt, + pubrel->prrelid); } systable_endscan(scan); @@ -819,6 +870,34 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) return result; } +/* + * Gets list of relation oids that are associated with a publication. + * + * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES + * should use GetAllPublicationRelations(). + */ +List * +GetIncludedPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) +{ + Assert(!GetPublication(pubid)->alltables); + + return get_publication_relations(pubid, pub_partopt, false); +} + +/* + * Gets list of table oids that were specified in the EXCEPT clause for a + * publication. + * + * This should only be used FOR ALL TABLES publications. + */ +List * +GetExcludedPublicationTables(Oid pubid, PublicationPartOpt pub_partopt) +{ + Assert(GetPublication(pubid)->alltables); + + return get_publication_relations(pubid, pub_partopt, true); +} + /* * Gets list of publication oids for publications marked as FOR ALL TABLES. */ @@ -858,24 +937,34 @@ GetAllTablesPublications(void) /* * Gets list of all relations published by FOR ALL TABLES/SEQUENCES - * publication(s). + * publication. * * If the publication publishes partition changes via their respective root * partitioned tables, we must exclude partitions in favor of including the * root partitioned tables. This is not applicable to FOR ALL SEQUENCES * publication. + * + * For a FOR ALL TABLES publication, the returned list excludes tables mentioned + * in EXCEPT TABLE clause. */ List * -GetAllPublicationRelations(char relkind, bool pubviaroot) +GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot) { Relation classRel; ScanKeyData key[1]; TableScanDesc scan; HeapTuple tuple; List *result = NIL; + List *exceptlist = NIL; Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot)); + /* EXCEPT filtering applies only to relations, not sequences */ + if (relkind == RELKIND_RELATION) + exceptlist = GetExcludedPublicationTables(pubid, pubviaroot ? + PUBLICATION_PART_ROOT : + PUBLICATION_PART_LEAF); + classRel = table_open(RelationRelationId, AccessShareLock); ScanKeyInit(&key[0], @@ -891,7 +980,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot) Oid relid = relForm->oid; if (is_publishable_class(relid, relForm) && - !(relForm->relispartition && pubviaroot)) + !(relForm->relispartition && pubviaroot) && + !list_member_oid(exceptlist, relid)) result = lappend_oid(result, relid); } @@ -912,7 +1002,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot) Oid relid = relForm->oid; if (is_publishable_class(relid, relForm) && - !relForm->relispartition) + !relForm->relispartition && + !list_member_oid(exceptlist, relid)) result = lappend_oid(result, relid); } @@ -1168,17 +1259,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) * those. Otherwise, get the partitioned table itself. */ if (pub_elem->alltables) - pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION, + pub_elem_tables = GetAllPublicationRelations(pub_elem->oid, + RELKIND_RELATION, pub_elem->pubviaroot); else { List *relids, *schemarelids; - relids = GetPublicationRelations(pub_elem->oid, - pub_elem->pubviaroot ? - PUBLICATION_PART_ROOT : - PUBLICATION_PART_LEAF); + relids = GetIncludedPublicationRelations(pub_elem->oid, + pub_elem->pubviaroot ? + PUBLICATION_PART_ROOT : + PUBLICATION_PART_LEAF); schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid, pub_elem->pubviaroot ? PUBLICATION_PART_ROOT : @@ -1367,7 +1459,9 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS) publication = GetPublicationByName(pubname, false); if (publication->allsequences) - sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false); + sequences = GetAllPublicationRelations(publication->oid, + RELKIND_SEQUENCE, + false); funcctx->user_fctx = sequences; diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index fc3a4c19e65..6a3ca4751fa 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -181,7 +181,7 @@ parse_publication_options(ParseState *pstate, */ static void ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, - List **rels, List **schemas) + List **rels, List **exceptrels, List **schemas) { ListCell *cell; PublicationObjSpec *pubobj; @@ -198,7 +198,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, switch (pubobj->pubobjtype) { + case PUBLICATIONOBJ_EXCEPT_TABLE: + pubobj->pubtable->except = true; + *exceptrels = lappend(*exceptrels, pubobj->pubtable); + break; case PUBLICATIONOBJ_TABLE: + pubobj->pubtable->except = false; *rels = lappend(*rels, pubobj->pubtable); break; case PUBLICATIONOBJ_TABLES_IN_SCHEMA: @@ -519,8 +524,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables) * a target. However, WAL records for TRUNCATE specify both a root and * its leaves. */ - relids = GetPublicationRelations(pubid, - PUBLICATION_PART_ALL); + relids = GetIncludedPublicationRelations(pubid, + PUBLICATION_PART_ALL); schemarelids = GetAllSchemaPublicationRelations(pubid, PUBLICATION_PART_ALL); @@ -844,6 +849,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) char publish_generated_columns; AclResult aclresult; List *relations = NIL; + List *exceptrelations = NIL; List *schemaidlist = NIL; /* must have CREATE privilege on database */ @@ -929,8 +935,21 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) CommandCounterIncrement(); /* Associate objects with the publication. */ + ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, + &exceptrelations, &schemaidlist); + if (stmt->for_all_tables) { + /* Process EXCEPT table list */ + if (exceptrelations != NIL) + { + List *rels; + + rels = OpenTableList(exceptrelations); + PublicationAddTables(puboid, rels, true, NULL); + CloseTableList(rels); + } + /* * Invalidate relcache so that publication info is rebuilt. Sequences * publication doesn't require invalidation, as replica identity @@ -940,9 +959,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) } else if (!stmt->for_all_sequences) { - ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, - &schemaidlist); - /* FOR TABLES IN SCHEMA requires superuser */ if (schemaidlist != NIL && !superuser()) ereport(ERROR, @@ -1050,8 +1066,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, LockDatabaseObject(PublicationRelationId, pubform->oid, 0, AccessShareLock); - root_relids = GetPublicationRelations(pubform->oid, - PUBLICATION_PART_ROOT); + root_relids = GetIncludedPublicationRelations(pubform->oid, + PUBLICATION_PART_ROOT); foreach(lc, root_relids) { @@ -1170,8 +1186,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, * trees, not just those explicitly mentioned in the publication. */ if (root_relids == NIL) - relids = GetPublicationRelations(pubform->oid, - PUBLICATION_PART_ALL); + relids = GetIncludedPublicationRelations(pubform->oid, + PUBLICATION_PART_ALL); else { /* @@ -1256,8 +1272,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, PublicationDropTables(pubid, rels, false); else /* AP_SetObjects */ { - List *oldrelids = GetPublicationRelations(pubid, - PUBLICATION_PART_ROOT); + List *oldrelids = GetIncludedPublicationRelations(pubid, + PUBLICATION_PART_ROOT); List *delrels = NIL; ListCell *oldlc; @@ -1358,6 +1374,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, oldrel = palloc_object(PublicationRelInfo); oldrel->whereClause = NULL; oldrel->columns = NIL; + oldrel->except = false; oldrel->relation = table_open(oldrelid, ShareUpdateExclusiveLock); delrels = lappend(delrels, oldrel); @@ -1408,7 +1425,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt, ListCell *lc; List *reloids; - reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT); + reloids = GetIncludedPublicationRelations(pubform->oid, + PUBLICATION_PART_ROOT); foreach(lc, reloids) { @@ -1566,11 +1584,15 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) else { List *relations = NIL; + List *exceptrelations = NIL; List *schemaidlist = NIL; Oid pubid = pubform->oid; ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, - &schemaidlist); + &exceptrelations, &schemaidlist); + + /* EXCEPT clause is not supported with ALTER PUBLICATION */ + Assert(exceptrelations == NIL); CheckAlterPublication(stmt, tup, relations, schemaidlist); @@ -1771,6 +1793,7 @@ OpenTableList(List *tables) pub_rel->relation = rel; pub_rel->whereClause = t->whereClause; pub_rel->columns = t->columns; + pub_rel->except = t->except; rels = lappend(rels, pub_rel); relids = lappend_oid(relids, myrelid); @@ -1843,6 +1866,7 @@ OpenTableList(List *tables) /* child inherits column list from parent */ pub_rel->columns = t->columns; + pub_rel->except = t->except; rels = lappend(rels, pub_rel); relids = lappend_oid(relids, childrelid); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index b04b0dbd2a0..85242dcc245 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8686,7 +8686,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, * expressions. */ if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && - GetRelationPublications(RelationGetRelid(rel)) != NIL) + GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"), @@ -18881,7 +18881,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged) * UNLOGGED, as UNLOGGED tables can't be published. */ if (!toLogged && - GetRelationPublications(RelationGetRelid(rel)) != NIL) + GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot change table \"%s\" to unlogged because it is part of a publication", @@ -20322,6 +20322,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, const char *trigger_name; Oid defaultPartOid; List *partBoundConstraint; + List *exceptpuboids = NIL; ParseState *pstate = make_parsestate(NULL); pstate->p_sourcetext = context->queryString; @@ -20361,6 +20362,49 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot attach a typed table as partition"))); + /* + * Disallow attaching a partition if the table is referenced in a + * publication EXCEPT clause. Changing the partition hierarchy could alter + * the effective publication membership. + */ + exceptpuboids = GetRelationExcludedPublications(RelationGetRelid(attachrel)); + if (exceptpuboids != NIL) + { + bool first = true; + StringInfoData pubnames; + + initStringInfo(&pubnames); + + foreach_oid(pubid, exceptpuboids) + { + char *pubname = get_publication_name(pubid, false); + + if (!first) + { + /* + * translator: This is a separator in a list of publication + * names. + */ + appendStringInfoString(&pubnames, _(", ")); + } + + first = false; + + appendStringInfo(&pubnames, _("\"%s\""), pubname); + } + + 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", + list_length(exceptpuboids), + RelationGetRelationName(attachrel), + pubnames.data), + errdetail("The publication EXCEPT clause cannot contain tables that are partitions.")); + } + + list_free(exceptpuboids); + /* * Table being attached should not already be part of inheritance; either * as a child table... diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c567252acc4..3c3e24324a8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -203,6 +203,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType, static PartitionStrategy parsePartitionStrategy(char *strategy, int location, core_yyscan_t yyscanner); static void preprocess_pub_all_objtype_list(List *all_objects_list, + List **pubobjects, bool *all_tables, bool *all_sequences, core_yyscan_t yyscanner); @@ -455,6 +456,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TriggerTransitions TriggerReferencing vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list pub_all_obj_type_list + pub_except_obj_list opt_pub_except_clause %type returning_clause %type returning_option @@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type var_value zone_value %type auth_ident RoleSpec opt_granted_by %type PublicationObjSpec +%type PublicationExceptObjSpec %type PublicationAllObjSpec %type unreserved_keyword type_func_name_keyword @@ -10792,7 +10795,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec * * pub_all_obj_type is one of: * - * TABLES + * TABLES [EXCEPT TABLE ( table [, ...] )] * SEQUENCES * * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options] @@ -10818,7 +10821,8 @@ CreatePublicationStmt: CreatePublicationStmt *n = makeNode(CreatePublicationStmt); n->pubname = $3; - preprocess_pub_all_objtype_list($5, &n->for_all_tables, + preprocess_pub_all_objtype_list($5, &n->pubobjects, + &n->for_all_tables, &n->for_all_sequences, yyscanner); n->options = $6; @@ -10933,11 +10937,17 @@ pub_obj_list: PublicationObjSpec { $$ = lappend($1, $3); } ; +opt_pub_except_clause: + EXCEPT TABLE '(' pub_except_obj_list ')' { $$ = $4; } + | /*EMPTY*/ { $$ = NIL; } + ; + PublicationAllObjSpec: - ALL TABLES + ALL TABLES opt_pub_except_clause { $$ = makeNode(PublicationAllObjSpec); $$->pubobjtype = PUBLICATION_ALL_TABLES; + $$->except_tables = $3; $$->location = @1; } | ALL SEQUENCES @@ -10954,6 +10964,23 @@ pub_all_obj_type_list: PublicationAllObjSpec { $$ = lappend($1, $3); } ; +PublicationExceptObjSpec: + relation_expr + { + $$ = makeNode(PublicationObjSpec); + $$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE; + $$->pubtable = makeNode(PublicationTable); + $$->pubtable->except = true; + $$->pubtable->relation = $1; + $$->location = @1; + } + ; + +pub_except_obj_list: PublicationExceptObjSpec + { $$ = list_make1($1); } + | pub_except_obj_list ',' PublicationExceptObjSpec + { $$ = lappend($1, $3); } + ; /***************************************************************************** * @@ -19812,8 +19839,9 @@ parsePartitionStrategy(char *strategy, int location, core_yyscan_t yyscanner) * Also, checks if the pub_object_type has been specified more than once. */ static void -preprocess_pub_all_objtype_list(List *all_objects_list, bool *all_tables, - bool *all_sequences, core_yyscan_t yyscanner) +preprocess_pub_all_objtype_list(List *all_objects_list, List **pubobjects, + bool *all_tables, bool *all_sequences, + core_yyscan_t yyscanner) { if (!all_objects_list) return; @@ -19833,6 +19861,7 @@ preprocess_pub_all_objtype_list(List *all_objects_list, bool *all_tables, parser_errposition(obj->location)); *all_tables = true; + *pubobjects = list_concat(*pubobjects, obj->except_tables); } else if (obj->pubobjtype == PUBLICATION_ALL_SEQUENCES) { diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c index 7a49185d29d..857ebf7d6fb 100644 --- a/src/backend/replication/pgoutput/pgoutput.c +++ b/src/backend/replication/pgoutput/pgoutput.c @@ -2089,7 +2089,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation) if (!entry->replicate_valid) { Oid schemaId = get_rel_namespace(relid); - List *pubids = GetRelationPublications(relid); + List *pubids = GetRelationIncludedPublications(relid); /* * We don't acquire a lock on the namespace system table as we build @@ -2206,14 +2206,47 @@ get_rel_sync_entry(PGOutputData *data, Relation relation) */ if (pub->alltables) { - publish = true; - if (pub->pubviaroot && am_partition) + List *exceptpubids = NIL; + + if (am_partition) { List *ancestors = get_partition_ancestors(relid); + Oid last_ancestor_relid = llast_oid(ancestors); - pub_relid = llast_oid(ancestors); - ancestor_level = list_length(ancestors); + /* + * For a partition, changes are published via top-most + * ancestor when pubviaroot is true, so populate pub_relid + * accordingly. + */ + if (pub->pubviaroot) + { + pub_relid = last_ancestor_relid; + ancestor_level = list_length(ancestors); + } + + /* + * Only the top-most ancestor can appear in the EXCEPT + * clause. Therefore, for a partition, exclusion must be + * evaluated at the top-most ancestor. + */ + exceptpubids = GetRelationExcludedPublications(last_ancestor_relid); } + else + { + /* + * For a regular table or a root partitioned table, check + * exclusion on table itself. + */ + exceptpubids = GetRelationExcludedPublications(pub_relid); + } + + if (!list_member_oid(exceptpubids, pub->oid)) + publish = true; + + list_free(exceptpubids); + + if (!publish) + continue; } if (!publish) diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 6b634c9fff1..a1c88c6b1b6 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -5788,7 +5788,9 @@ RelationGetExclusionInfo(Relation indexRelation, void RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) { - List *puboids; + List *puboids = NIL; + List *exceptpuboids = NIL; + List *alltablespuboids; ListCell *lc; MemoryContext oldcxt; Oid schemaid; @@ -5826,28 +5828,49 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) pubdesc->gencols_valid_for_delete = true; /* Fetch the publication membership info. */ - puboids = GetRelationPublications(relid); + puboids = GetRelationIncludedPublications(relid); schemaid = RelationGetNamespace(relation); puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid)); 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); foreach(lc, ancestors) { Oid ancestor = lfirst_oid(lc); puboids = list_concat_unique_oid(puboids, - GetRelationPublications(ancestor)); + GetRelationIncludedPublications(ancestor)); schemaid = get_rel_namespace(ancestor); puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid)); } - } - puboids = list_concat_unique_oid(puboids, GetAllTablesPublications()); + /* + * Only the top-most ancestor can appear in the EXCEPT clause. + * Therefore, for a partition, exclusion must be evaluated at the + * top-most ancestor. + */ + exceptpuboids = GetRelationExcludedPublications(last_ancestor_relid); + } + else + { + /* + * For a regular table or a root partitioned table, check exclusion on + * table itself. + */ + exceptpuboids = GetRelationExcludedPublications(relid); + } + + alltablespuboids = GetAllTablesPublications(); + puboids = list_concat_unique_oid(puboids, + list_difference_oid(alltablespuboids, + exceptpuboids)); foreach(lc, puboids) { Oid pubid = lfirst_oid(lc); diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 6df79067db5..1035bba72ce 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -4623,9 +4623,59 @@ getPublications(Archive *fout) (strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0); pubinfo[i].pubgencols_type = *(PQgetvalue(res, i, i_pubgencols)); + pubinfo[i].except_tables = (SimplePtrList) + { + NULL, NULL + }; /* Decide whether we want to dump it */ selectDumpableObject(&(pubinfo[i].dobj), fout); + + /* + * Get the list of tables for publications specified in the EXCEPT + * TABLE clause. + * + * Although individual EXCEPT TABLE entries could be stored in + * PublicationRelInfo, dumpPublicationTable cannot be used to emit + * them, because there is no ALTER PUBLICATION ... ADD command to add + * individual table entries to the EXCEPT TABLE list. + * + * Therefore, the approach is to dump the complete EXCEPT TABLE list + * in a single CREATE PUBLICATION statement. PublicationInfo is used + * to collect this information, which is then emitted by + * dumpPublication(). + */ + if (fout->remoteVersion >= 190000) + { + int ntbls; + PGresult *res_tbls; + + resetPQExpBuffer(query); + appendPQExpBuffer(query, + "SELECT prrelid\n" + "FROM pg_catalog.pg_publication_rel\n" + "WHERE prpubid = %u AND prexcept", + pubinfo[i].dobj.catId.oid); + + res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntbls = PQntuples(res_tbls); + + for (int j = 0; j < ntbls; j++) + { + Oid prrelid; + TableInfo *tbinfo; + + prrelid = atooid(PQgetvalue(res_tbls, j, 0)); + + tbinfo = findTableByOid(prrelid); + + if (tbinfo != NULL) + simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo); + } + + PQclear(res_tbls); + } } cleanup: @@ -4662,10 +4712,29 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo) appendPQExpBuffer(query, "CREATE PUBLICATION %s", qpubname); - if (pubinfo->puballtables && pubinfo->puballsequences) - appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES"); - else if (pubinfo->puballtables) + if (pubinfo->puballtables) + { + int n_except = 0; + appendPQExpBufferStr(query, " FOR ALL TABLES"); + + /* Include EXCEPT TABLE clause if there are except_tables. */ + for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell; cell = cell->next) + { + TableInfo *tbinfo = (TableInfo *) cell->ptr; + + if (++n_except == 1) + appendPQExpBufferStr(query, " EXCEPT TABLE ("); + else + appendPQExpBufferStr(query, ", "); + appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo)); + } + if (n_except > 0) + appendPQExpBufferStr(query, ")"); + + if (pubinfo->puballsequences) + appendPQExpBufferStr(query, ", ALL SEQUENCES"); + } else if (pubinfo->puballsequences) appendPQExpBufferStr(query, " FOR ALL SEQUENCES"); @@ -4845,6 +4914,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) /* Collect all publication membership info. */ if (fout->remoteVersion >= 150000) + { appendPQExpBufferStr(query, "SELECT tableoid, oid, prpubid, prrelid, " "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, " @@ -4857,6 +4927,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) " WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n" " ELSE NULL END) prattrs " "FROM pg_catalog.pg_publication_rel pr"); + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, " WHERE NOT pr.prexcept"); + } else appendPQExpBufferStr(query, "SELECT tableoid, oid, prpubid, prrelid, " diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 6deceef23f3..e138ef1276c 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -676,6 +676,7 @@ typedef struct _PublicationInfo bool pubtruncate; bool pubviaroot; PublishGencolsType pubgencols_type; + SimplePtrList except_tables; } PublicationInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index f15bd06adcc..588891a62bd 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3202,6 +3202,36 @@ my %tests = ( like => { %full_runs, section_post_data => 1, }, }, + 'CREATE PUBLICATION pub8' => { + create_order => 50, + create_sql => + 'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table);', + regexp => qr/^ + \QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub9' => { + create_order => 50, + create_sql => + 'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);', + regexp => qr/^ + \QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub10' => { + create_order => 92, + create_sql => + 'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_inheritance_parent);', + regexp => qr/^ + \QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_inheritance_parent, ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + 'CREATE SUBSCRIPTION sub1' => { create_order => 50, create_sql => 'CREATE SUBSCRIPTION sub1 diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index ab13c90ed33..a94eade282f 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3075,15 +3075,43 @@ describeOneTableDetails(const char *schemaname, "FROM pg_catalog.pg_publication p\n" " JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n" " JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n" - "WHERE pr.prrelid = '%s'\n" - "UNION\n" - "SELECT pubname\n" - " , NULL\n" - " , NULL\n" - "FROM pg_catalog.pg_publication p\n" - "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n" - "ORDER BY 1;", - oid, oid, oid, oid); + "WHERE pr.prrelid = '%s'\n", + oid, oid, oid); + + if (pset.sversion >= 190000) + { + /* + * Skip entries where this relation appears in the + * publication's EXCEPT TABLE list. + */ + appendPQExpBuffer(&buf, + " AND NOT pr.prexcept\n" + "UNION\n" + "SELECT pubname\n" + " , NULL\n" + " , NULL\n" + "FROM pg_catalog.pg_publication p\n" + "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n" + " AND NOT EXISTS (\n" + " SELECT 1\n" + " FROM pg_catalog.pg_publication_rel pr\n" + " WHERE pr.prpubid = p.oid AND\n" + " (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s')))\n" + "ORDER BY 1;", + oid, oid, oid); + } + else + { + appendPQExpBuffer(&buf, + "UNION\n" + "SELECT pubname\n" + " , NULL\n" + " , NULL\n" + "FROM pg_catalog.pg_publication p\n" + "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n" + "ORDER BY 1;", + oid); + } } else { @@ -3134,6 +3162,36 @@ describeOneTableDetails(const char *schemaname, PQclear(result); } + /* Print publications where the table is in the EXCEPT clause */ + if (pset.sversion >= 190000) + { + printfPQExpBuffer(&buf, + "SELECT pubname\n" + "FROM pg_catalog.pg_publication p\n" + "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n" + "WHERE (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s'))\n" + "AND pr.prexcept\n" + "ORDER BY 1;", oid, oid); + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + printTableAddFooter(&cont, _("Except Publications:")); + + /* Might be an empty set - that's ok */ + for (i = 0; i < tuples; i++) + { + printfPQExpBuffer(&buf, " \"%s\"", PQgetvalue(result, i, 0)); + + printTableAddFooter(&cont, buf.data); + } + PQclear(result); + } + /* * If verbose, print NOT NULL constraints. */ @@ -6763,8 +6821,12 @@ describePublications(const char *pattern) " pg_catalog.pg_publication_rel pr\n" "WHERE c.relnamespace = n.oid\n" " AND c.oid = pr.prrelid\n" - " AND pr.prpubid = '%s'\n" - "ORDER BY 1,2", pubid); + " AND pr.prpubid = '%s'\n", pubid); + + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, " AND NOT pr.prexcept\n"); + + appendPQExpBuffer(&buf, "ORDER BY 1,2"); if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont)) goto error_return; @@ -6782,6 +6844,23 @@ describePublications(const char *pattern) goto error_return; } } + else + { + if (pset.sversion >= 190000) + { + /* Get tables in the EXCEPT clause for this publication */ + printfPQExpBuffer(&buf, + "SELECT n.nspname || '.' || c.relname\n" + "FROM pg_catalog.pg_class c\n" + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" + " JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n" + "WHERE pr.prpubid = '%s' AND pr.prexcept\n" + "ORDER BY 1", pubid); + if (!addFooterToPublicationDesc(&buf, _("Except tables:"), + true, &cont)) + goto error_return; + } + } printTable(&cont, pset.queryFout, false, pset.logfile); printTableCleanup(&cont); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 905c076763c..f8c0865ca89 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3681,7 +3681,17 @@ match_previous_words(int pattern_id, else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL")) COMPLETE_WITH("TABLES", "SEQUENCES"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES")) - COMPLETE_WITH("WITH ("); + COMPLETE_WITH("EXCEPT TABLE (", "WITH ("); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT")) + COMPLETE_WITH("TABLE ("); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE")) + COMPLETE_WITH("("); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ',')) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ',')) + COMPLETE_WITH(")"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES")) COMPLETE_WITH("IN SCHEMA"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ',')) diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 7670eb226f0..f164bf8767f 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202602201 +#define CATALOG_VERSION_NO 202603041 #endif diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h index 6e5f73caa9e..e25228713e7 100644 --- a/src/include/catalog/pg_publication.h +++ b/src/include/catalog/pg_publication.h @@ -150,16 +150,19 @@ typedef struct PublicationRelInfo Relation relation; Node *whereClause; List *columns; + bool except; } PublicationRelInfo; extern Publication *GetPublication(Oid pubid); extern Publication *GetPublicationByName(const char *pubname, bool missing_ok); -extern List *GetRelationPublications(Oid relid); +extern List *GetRelationIncludedPublications(Oid relid); +extern List *GetRelationExcludedPublications(Oid relid); /*--------- - * Expected values for pub_partopt parameter of GetPublicationRelations(), - * which allows callers to specify which partitions of partitioned tables - * mentioned in the publication they expect to see. + * Expected values for pub_partopt parameter of + * GetIncludedPublicationRelations(), which allows callers to specify which + * partitions of partitioned tables mentioned in the publication they expect to + * see. * * ROOT: only the table explicitly mentioned in the publication * LEAF: only leaf partitions in given tree @@ -172,9 +175,12 @@ typedef enum PublicationPartOpt PUBLICATION_PART_ALL, } PublicationPartOpt; -extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt); +extern List *GetIncludedPublicationRelations(Oid pubid, + PublicationPartOpt pub_partopt); +extern List *GetExcludedPublicationTables(Oid pubid, + PublicationPartOpt pub_partopt); extern List *GetAllTablesPublications(void); -extern List *GetAllPublicationRelations(char relkind, bool pubviaroot); +extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot); extern List *GetPublicationSchemas(Oid pubid); extern List *GetSchemaPublications(Oid schemaid); extern List *GetSchemaPublicationRelations(Oid schemaid, diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h index 63eb7c75f53..cefc38c9ed8 100644 --- a/src/include/catalog/pg_publication_rel.h +++ b/src/include/catalog/pg_publication_rel.h @@ -33,6 +33,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId) Oid oid; /* oid */ Oid prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */ Oid prrelid BKI_LOOKUP(pg_class); /* Oid of the relation */ + bool prexcept BKI_DEFAULT(f); /* relation is not published */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ pg_node_tree prqual; /* qualifications */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index f37131835be..ff41943a6db 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4299,9 +4299,10 @@ typedef struct AlterTSConfigurationStmt typedef struct PublicationTable { NodeTag type; - RangeVar *relation; /* relation to be published */ + RangeVar *relation; /* publication relation */ Node *whereClause; /* qualifications */ List *columns; /* List of columns in a publication table */ + bool except; /* True if listed in the EXCEPT clause */ } PublicationTable; /* @@ -4310,6 +4311,7 @@ typedef struct PublicationTable typedef enum PublicationObjSpecType { PUBLICATIONOBJ_TABLE, /* A table */ + PUBLICATIONOBJ_EXCEPT_TABLE, /* A table in the EXCEPT clause */ PUBLICATIONOBJ_TABLES_IN_SCHEMA, /* All tables in schema */ PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA, /* All tables in first element of * search_path */ @@ -4338,6 +4340,7 @@ typedef struct PublicationAllObjSpec { NodeTag type; PublicationAllObjType pubobjtype; /* type of this publication object */ + List *except_tables; /* tables specified in the EXCEPT clause */ ParseLoc location; /* token location, or -1 if unknown */ } PublicationAllObjSpec; diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 105d15ae45f..681d2564ed5 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -213,33 +213,157 @@ Not-null constraints: regress_publication_user | t | f | t | t | f | f | none | f | (1 row) -DROP TABLE testpub_tbl2; -DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema; -CREATE TABLE testpub_tbl3 (a int); -CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3); +--------------------------------------------- +-- EXCEPT TABLE tests for normal tables +--------------------------------------------- SET client_min_messages = 'ERROR'; -CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3; -CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3; +-- Specify table list in the EXCEPT TABLE clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2); +\dRp+ testpub_foralltables_excepttable + Publication testpub_foralltables_excepttable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl1" + "public.testpub_tbl2" + +-- Specify table in the EXCEPT TABLE clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT TABLE (testpub_tbl1); +\dRp+ testpub_foralltables_excepttable1 + Publication testpub_foralltables_excepttable1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl1" + +-- Check that the table description shows the publications where it is listed +-- in the EXCEPT TABLE clause +\d testpub_tbl1 + Table "public.testpub_tbl1" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+------------------------------------------ + id | integer | | not null | nextval('testpub_tbl1_id_seq'::regclass) + data | text | | | +Indexes: + "testpub_tbl1_pkey" PRIMARY KEY, btree (id) +Publications: + "testpub_foralltables" +Except Publications: + "testpub_foralltables_excepttable" + "testpub_foralltables_excepttable1" + RESET client_min_messages; +DROP TABLE testpub_tbl2; +DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1; +--------------------------------------------- +-- Tests for inherited tables, and +-- EXCEPT TABLE tests for inherited tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_tbl_parent (a int); +CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent); +CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent; \dRp+ testpub3 Publication testpub3 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_tbl3" - "public.testpub_tbl3a" + "public.testpub_tbl_child" + "public.testpub_tbl_parent" +CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent; \dRp+ testpub4 Publication testpub4 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_tbl3" + "public.testpub_tbl_parent" -DROP TABLE testpub_tbl3, testpub_tbl3a; -DROP PUBLICATION testpub3, testpub4; +-- List the parent table in the EXCEPT TABLE clause (without ONLY or '*') +CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent); +\dRp+ testpub5 + Publication testpub5 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl_child" + "public.testpub_tbl_parent" + +-- EXCEPT with '*': list the table and all its descendants in the EXCEPT TABLE clause +CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *); +\dRp+ testpub6 + Publication testpub6 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl_child" + "public.testpub_tbl_parent" + +-- EXCEPT with ONLY: list the table in the EXCEPT TABLE clause, but not its descendants +CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent); +\dRp+ testpub7 + Publication testpub7 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl_parent" + +RESET client_min_messages; +DROP TABLE testpub_tbl_parent, testpub_tbl_child; +DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7; +--------------------------------------------- +-- EXCEPT TABLE tests for partitioned tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a); +CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100); +CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT TABLE (testpub_root); +\dRp+ testpub8; + Publication testpub8 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_root" + +\d testpub_part1 + Table "public.testpub_part1" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | +Partition of: testpub_root FOR VALUES FROM (0) TO (100) +Except Publications: + "testpub8" + +\d testpub_root + Partitioned table "public.testpub_root" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | +Partition key: RANGE (a) +Except Publications: + "testpub8" +Number of partitions: 1 (Use \d+ to list them.) + +CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT TABLE (testpub_part1); +ERROR: cannot use publication EXCEPT clause for relation "testpub_part1" +DETAIL: This operation is not supported for individual partitions. +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 TABLE 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 +DETAIL: The publication EXCEPT clause cannot contain tables that are partitions. +RESET client_min_messages; +DROP TABLE testpub_root, testpub_part1, tab_main; +DROP PUBLICATION testpub8; --- Tests for publications with SEQUENCES CREATE SEQUENCE regress_pub_seq0; CREATE SEQUENCE pub_test.regress_pub_seq1; diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 85b00bd67c8..405579dad52 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -105,20 +105,69 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall \d+ testpub_tbl2 \dRp+ testpub_foralltables -DROP TABLE testpub_tbl2; -DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema; - -CREATE TABLE testpub_tbl3 (a int); -CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3); +--------------------------------------------- +-- EXCEPT TABLE tests for normal tables +--------------------------------------------- SET client_min_messages = 'ERROR'; -CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3; -CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3; -RESET client_min_messages; -\dRp+ testpub3 -\dRp+ testpub4 +-- Specify table list in the EXCEPT TABLE clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2); +\dRp+ testpub_foralltables_excepttable +-- Specify table in the EXCEPT TABLE clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT TABLE (testpub_tbl1); +\dRp+ testpub_foralltables_excepttable1 +-- Check that the table description shows the publications where it is listed +-- in the EXCEPT TABLE clause +\d testpub_tbl1 -DROP TABLE testpub_tbl3, testpub_tbl3a; -DROP PUBLICATION testpub3, testpub4; +RESET client_min_messages; +DROP TABLE testpub_tbl2; +DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1; + +--------------------------------------------- +-- Tests for inherited tables, and +-- EXCEPT TABLE tests for inherited tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_tbl_parent (a int); +CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent); +CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent; +\dRp+ testpub3 +CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent; +\dRp+ testpub4 +-- List the parent table in the EXCEPT TABLE clause (without ONLY or '*') +CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent); +\dRp+ testpub5 +-- EXCEPT with '*': list the table and all its descendants in the EXCEPT TABLE clause +CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *); +\dRp+ testpub6 +-- EXCEPT with ONLY: list the table in the EXCEPT TABLE clause, but not its descendants +CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent); +\dRp+ testpub7 + +RESET client_min_messages; +DROP TABLE testpub_tbl_parent, testpub_tbl_child; +DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7; + +--------------------------------------------- +-- EXCEPT TABLE tests for partitioned tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a); +CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100); +CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT TABLE (testpub_root); +\dRp+ testpub8; +\d testpub_part1 +\d testpub_root +CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT TABLE (testpub_part1); + +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 TABLE clause. +ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200); + +RESET client_min_messages; +DROP TABLE testpub_root, testpub_part1, tab_main; +DROP PUBLICATION testpub8; --- Tests for publications with SEQUENCES CREATE SEQUENCE regress_pub_seq0; diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index a4c7dbaff59..f4a9cf5057f 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -46,6 +46,7 @@ tests += { 't/034_temporal.pl', 't/035_conflicts.pl', 't/036_sequences.pl', + 't/037_except.pl', 't/100_bugs.pl', ], }, diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl new file mode 100644 index 00000000000..2729df4d5c0 --- /dev/null +++ b/src/test/subscription/t/037_except.pl @@ -0,0 +1,255 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Logical replication tests for EXCEPT TABLE publications +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; + +# Initialize subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +my $result; + +sub test_except_root_partition +{ + my ($pubviaroot) = @_; + + # If the root partitioned table is in the EXCEPT TABLE 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); + INSERT INTO root1 VALUES (1), (101); + )); + $node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part" + ); + $node_subscriber->wait_for_subscription_sync($node_publisher, + 'tap_sub_part'); + + # Advance the replication slot to ignore changes generated before this point. + $node_publisher->safe_psql('postgres', + "SELECT slot_name FROM pg_replication_slot_advance('test_slot', pg_current_wal_lsn())" + ); + $node_publisher->safe_psql('postgres', + "INSERT INTO root1 VALUES (2), (102)"); + + # Verify that data inserted into the partitioned table is not published when + # it is in the EXCEPT TABLE clause. + $result = $node_publisher->safe_psql('postgres', + "SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tap_pub_part')" + ); + $node_publisher->wait_for_catchup('tap_sub_part'); + + # Verify that no rows are replicated to subscriber for root or partitions. + foreach my $table (qw(root1 part1 part2 part2_1)) + { + $result = $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM $table"); + is($result, qq(0), "no rows replicated to subscriber for $table"); + } + + $node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part"); + $node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part"); +} + +# ============================================ +# EXCEPT TABLE test cases for non-partitioned tables and inherited tables. +# ============================================ + +# Create schemas and tables on publisher +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab1 AS SELECT generate_series(1,10) AS a; + CREATE TABLE parent (a int); + CREATE TABLE child (b int) INHERITS (parent); + CREATE TABLE parent1 (a int); + CREATE TABLE child1 (b int) INHERITS (parent1); +)); + +# Create schemas and tables on subscriber +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE TABLE tab1 (a int); + CREATE TABLE parent (a int); + CREATE TABLE child (b int) INHERITS (parent); + CREATE TABLE parent1 (a int); + CREATE TABLE child1 (b int) INHERITS (parent1); +)); + +# Exclude tab1 (non-inheritance case), and also exclude parent and ONLY parent1 +# to verify exclusion behavior for inherited tables, including the effect of +# ONLY in the EXCEPT TABLE clause. +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION tab_pub FOR ALL TABLES EXCEPT TABLE (tab1, parent, only parent1)" +); + +# Create a logical replication slot to help with later tests. +$node_publisher->safe_psql('postgres', + "SELECT pg_create_logical_replication_slot('test_slot', 'pgoutput')"); + +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tab_sub CONNECTION '$publisher_connstr' PUBLICATION tab_pub" +); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tab_sub'); + +# Check the table data does not sync for the tables specified in EXCEPT TABLE +# clause. +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1"); +is($result, qq(0), + 'check there is no initial data copied for the tables specified in the EXCEPT TABLE clause' +); + +# Insert some data into the table listed in the EXCEPT TABLE clause +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO tab1 VALUES(generate_series(11,20)); + INSERT INTO child VALUES(generate_series(11,20), generate_series(11,20)); +)); + +# Verify that data inserted into a table listed in the EXCEPT TABLE clause is +# not published. +$result = $node_publisher->safe_psql('postgres', + "SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tab_pub')" +); +is($result, qq(t), + 'verify no changes for table listed in the EXCEPT TABLE clause are present in the replication slot' +); + +# This should be published because ONLY parent1 was specified in the +# EXCEPT TABLE clause, so the exclusion applies only to the parent table and not +# to its child. +$node_publisher->safe_psql('postgres', + "INSERT INTO child1 VALUES(generate_series(11,20), generate_series(11,20))" +); + +# Verify that data inserted into a table listed in the EXCEPT TABLE clause is +# not replicated. +$node_publisher->wait_for_catchup('tab_sub'); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1"); +is($result, qq(0), 'check replicated inserts on subscriber'); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM child"); +is($result, qq(0), 'check replicated inserts on subscriber'); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM child1"); +is($result, qq(10), 'check replicated inserts on subscriber'); + +# cleanup +$node_subscriber->safe_psql( + 'postgres', qq( + DROP SUBSCRIPTION tab_sub; + TRUNCATE TABLE tab1; + DROP TABLE parent, parent1, child, child1; +)); +$node_publisher->safe_psql( + 'postgres', qq( + DROP PUBLICATION tab_pub; + TRUNCATE TABLE tab1; + DROP TABLE parent, parent1, child, child1; +)); + +# ============================================ +# EXCEPT TABLE test cases for partitioned tables +# ============================================ +# Setup partitioned table and partitions on the publisher that map to normal +# tables on the subscriber. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE root1(a int) PARTITION BY RANGE(a); + CREATE TABLE part1 PARTITION OF root1 FOR VALUES FROM (0) TO (100); + CREATE TABLE part2 PARTITION OF root1 FOR VALUES FROM (100) TO (200) PARTITION BY RANGE(a); + CREATE TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (100) TO (150); +)); + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE TABLE root1(a int); + CREATE TABLE part1(a int); + CREATE TABLE part2(a int); + CREATE TABLE part2_1(a int); +)); + +# Validate the behaviour with both publish_via_partition_root as true and false +test_except_root_partition('false'); +test_except_root_partition('true'); + +# ============================================ +# Test when a subscription is subscribing to multiple publications +# ============================================ + +# OK when a table is excluded by pub1 EXCEPT TABLE, but it is included by pub2 +# FOR TABLE. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub1 FOR ALL TABLES EXCEPT TABLE (tab1); + CREATE PUBLICATION tap_pub2 FOR TABLE tab1; + INSERT INTO tab1 VALUES(1); +)); +$node_subscriber->psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2))); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_publisher->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a"); +is( $result, qq(1 +2), + "check replication of a table in the EXCEPT TABLE clause of one publication but included by another" +); +$node_publisher->safe_psql( + 'postgres', qq( + DROP PUBLICATION tap_pub2; + TRUNCATE tab1; +)); +$node_subscriber->safe_psql('postgres', qq(TRUNCATE tab1)); + +# OK when a table is excluded by pub1 EXCEPT TABLE, but it is included by pub2 +# FOR ALL TABLES. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub2 FOR ALL TABLES; + INSERT INTO tab1 VALUES(1); +)); +$node_subscriber->psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2))); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_publisher->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a"); +is( $result, qq(1 +2), + "check replication of a table in the EXCEPT TABLE clause of one publication but included by another" +); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2'); + +$node_publisher->stop('fast'); + +done_testing();