From 110244aa38bc27da051c0b13ee3a79d689ccaa2c Mon Sep 17 00:00:00 2001 From: amit Date: Wed, 2 Oct 2019 18:52:49 +0900 Subject: [PATCH v2] Support adding partitioned tables to publication Adding a partitioned table to a publication in turn adds all of its existing and future partitions. Detaching a partition doesn't remove it from the publication, but its membership is dissociated from the parent's membership, that is, it becomes a standalone member. --- doc/src/sgml/logical-replication.sgml | 22 +- doc/src/sgml/ref/alter_publication.sgml | 11 +- doc/src/sgml/ref/create_publication.sgml | 12 +- src/backend/catalog/pg_publication.c | 89 +++++-- src/backend/commands/publicationcmds.c | 394 +++++++++++++++++++--------- src/backend/commands/tablecmds.c | 13 +- src/backend/executor/execReplication.c | 19 +- src/backend/replication/logical/tablesync.c | 21 +- src/bin/pg_dump/pg_dump.c | 20 +- src/include/catalog/pg_publication.h | 6 +- src/include/catalog/pg_publication_rel.h | 1 + src/include/commands/publicationcmds.h | 2 + src/include/replication/logicalproto.h | 1 + src/test/regress/expected/publication.out | 152 ++++++++++- src/test/regress/sql/publication.sql | 80 +++++- 15 files changed, 655 insertions(+), 188 deletions(-) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index f657d1d06e..c14861ddfb 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -402,13 +402,21 @@ - Replication is only possible from base tables to base tables. That is, - the tables on the publication and on the subscription side must be normal - tables, not views, materialized views, partition root tables, or foreign - tables. In the case of partitions, you can therefore replicate a - partition hierarchy one-to-one, but you cannot currently replicate to a - differently partitioned setup. Attempts to replicate tables other than - base tables will result in an error. + Replication is only possible between combinations of regular and + partitioned tables. That is, the tables on the publication and on the + subscription side must be normal or partitioned tables, not views, + materialized views, or foreign tables. Attempts to replicate tables other + than regular and partitioned tables will result in an error. + + + + Actually, when a partitioned table is added to a publication, all of its + existing and future partitions are automatically added to the publication. + Any changes made to the leaf partitions are sent to the subscription server + which must contain a partitioned table with partition hierarchy matching + one-to-one with the publication side partitioned table. For partitioned + tables on the two sides to match one-to-one, each partition with a given + partition constraint must have the same name on both sides. diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml index 534e598d93..e9db773d9b 100644 --- a/doc/src/sgml/ref/alter_publication.sgml +++ b/doc/src/sgml/ref/alter_publication.sgml @@ -46,7 +46,11 @@ ALTER PUBLICATION name RENAME TO ALTER SUBSCRIPTION ... REFRESH PUBLICATION action on the subscribing side in order - to become effective. + to become effective. Using DROP TABLE to remove a + partitioned table from a publication will also remove all of its partitions + from the publication unless ONLY is specified. However, + removing a partition from a publication without first removing its parent + will result in an error. @@ -91,7 +95,10 @@ ALTER PUBLICATION name RENAME TO ONLY is not specified, the table and all its descendant tables (if any) are affected. Optionally, * can be specified after the table - name to explicitly indicate that descendant tables are included. + name to explicitly indicate that descendant tables are included. Specifying + ONLY with SET TABLE will result in an + error for a partitioned table if it contains partitions, because partitions + must be added to the publication too. diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml index 99f87ca393..7354665e47 100644 --- a/doc/src/sgml/ref/create_publication.sgml +++ b/doc/src/sgml/ref/create_publication.sgml @@ -72,11 +72,13 @@ CREATE PUBLICATION name - Only persistent base tables can be part of a publication. Temporary - tables, unlogged tables, foreign tables, materialized views, regular - views, and partitioned tables cannot be part of a publication. To - replicate a partitioned table, add the individual partitions to the - publication. + Only persistent base and partitioned tables can be part of a publication. + Temporary tables, unlogged tables, foreign tables, materialized views, + regular views cannot be part of a publication. Specifying + ONLY results in an error for a partitioned table if + it contains partitions, because partitions must be added to the + publication too. See + for details about how partitioned tables are replicated. diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index fd5da7d5f7..2547cb71f8 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -50,17 +50,9 @@ static void check_publication_add_relation(Relation targetrel) { - /* Give more specific error for partitioned tables */ - if (RelationGetForm(targetrel)->relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("\"%s\" is a partitioned table", - RelationGetRelationName(targetrel)), - errdetail("Adding partitioned tables to publications is not supported."), - errhint("You can add the table partitions individually."))); - /* Must be table */ - if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION) + if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION && + RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("\"%s\" is not a table", @@ -106,7 +98,8 @@ check_publication_add_relation(Relation targetrel) static bool is_publishable_class(Oid relid, Form_pg_class reltuple) { - return reltuple->relkind == RELKIND_RELATION && + return (reltuple->relkind == RELKIND_RELATION || + reltuple->relkind == RELKIND_PARTITIONED_TABLE) && !IsCatalogRelationOid(relid) && reltuple->relpersistence == RELPERSISTENCE_PERMANENT && relid >= FirstNormalObjectId; @@ -144,13 +137,56 @@ pg_relation_is_publishable(PG_FUNCTION_ARGS) PG_RETURN_BOOL(result); } +/* + * Update prinh flag for a given relation's pg_publication_rel entry + */ +void +publication_rel_update_inheritance(Relation pubrelCatalog, Oid pubid, + Relation rel, bool inh) +{ + Oid relid = RelationGetRelid(rel); + HeapTuple tuple; + Form_pg_publication_rel prform; + + Assert(pubrelCatalog != NULL); + + tuple = SearchSysCache2(PUBLICATIONRELMAP, ObjectIdGetDatum(relid), + ObjectIdGetDatum(pubid)); + Assert(tuple != NULL); + + prform = (Form_pg_publication_rel) GETSTRUCT(tuple); + if (prform->prinh != inh) + { + Datum newValues[Natts_pg_publication_rel]; + bool newNulls[Natts_pg_publication_rel]; + bool replaces[Natts_pg_publication_rel]; + HeapTuple newTuple; + + MemSet(newValues, 0, sizeof(newValues)); + MemSet(newNulls, false, sizeof(newValues)); + MemSet(replaces, false, sizeof(replaces)); + newValues[Anum_pg_publication_rel_prinh - 1] = inh; + newNulls[Anum_pg_publication_rel_prinh - 1] = false; + replaces[Anum_pg_publication_rel_prinh - 1] = true; + + newTuple = heap_modify_tuple(tuple, + RelationGetDescr(pubrelCatalog), + newValues, newNulls, + replaces); + CatalogTupleUpdate(pubrelCatalog, &newTuple->t_self, newTuple); + heap_freetuple(newTuple); + } + + ReleaseSysCache(tuple); +} + /* * Insert new publication / relation mapping. */ ObjectAddress publication_add_relation(Oid pubid, Relation targetrel, - bool if_not_exists) + bool inh, bool if_not_exists) { Relation rel; HeapTuple tup; @@ -172,10 +208,20 @@ publication_add_relation(Oid pubid, Relation targetrel, if (SearchSysCacheExists2(PUBLICATIONRELMAP, ObjectIdGetDatum(relid), ObjectIdGetDatum(pubid))) { - table_close(rel, RowExclusiveLock); - - if (if_not_exists) + if (if_not_exists || inh) + { + /* + * It's possible that the target relation is being re-added to the + * publication due to inheritance recursion. In that case, simply + * set the inheritance flag of the found entry. Note that the + * flag is turned off when the partition is detached from the + * parent. + */ + if (inh) + publication_rel_update_inheritance(rel, pubid, targetrel, inh); + table_close(rel, RowExclusiveLock); return InvalidObjectAddress; + } ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), @@ -196,6 +242,9 @@ publication_add_relation(Oid pubid, Relation targetrel, ObjectIdGetDatum(pubid); values[Anum_pg_publication_rel_prrelid - 1] = ObjectIdGetDatum(relid); + /* Set inheritance only for partitions. */ + values[Anum_pg_publication_rel_prinh - 1] = + BoolGetDatum(inh && targetrel->rd_rel->relispartition); tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); @@ -254,9 +303,12 @@ GetRelationPublications(Oid relid) * * This should only be used for normal publications, the FOR ALL TABLES * should use GetAllTablesPublicationRelations(). + * + * Return partitions that were added to the publication via parent only + * if 'get_children' is true. */ List * -GetPublicationRelations(Oid pubid) +GetPublicationRelations(Oid pubid, bool get_children) { List *result; Relation pubrelsrel; @@ -282,7 +334,8 @@ GetPublicationRelations(Oid pubid) pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); - result = lappend_oid(result, pubrel->prrelid); + if (!pubrel->prinh || get_children) + result = lappend_oid(result, pubrel->prrelid); } systable_endscan(scan); @@ -497,7 +550,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) if (publication->alltables) tables = GetAllTablesPublicationRelations(); else - tables = GetPublicationRelations(publication->oid); + tables = GetPublicationRelations(publication->oid, true); funcctx->user_fctx = (void *) tables; MemoryContextSwitchTo(oldcontext); diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index f115d4bf80..9bd85b13de 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -37,6 +37,10 @@ #include "commands/event_trigger.h" #include "commands/publicationcmds.h" +#include "nodes/makefuncs.h" + +#include "partitioning/partdesc.h" + #include "utils/array.h" #include "utils/builtins.h" #include "utils/catcache.h" @@ -50,11 +54,16 @@ /* Same as MAXNUMMESSAGES in sinvaladt.c */ #define MAX_RELCACHE_INVAL_MSGS 4096 -static List *OpenTableList(List *tables); -static void CloseTableList(List *rels); -static void PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, +static void PublicationAddTables(Oid pubid, List *tables, bool if_not_exists, AlterPublicationStmt *stmt); +static void PublicationAddTable(Oid pubid, Relation rel, bool if_not_exists, + AlterPublicationStmt *stmt, + bool recurse, bool recursing, + List **processed_relids); static void PublicationDropTables(Oid pubid, List *rels, bool missing_ok); +static void PublicationDropTable(Oid pubid, Relation rel, bool missing_ok, + bool recurse, bool recursing, + List **processed_relids); static void parse_publication_options(List *options, @@ -219,13 +228,8 @@ CreatePublication(CreatePublicationStmt *stmt) if (stmt->tables) { - List *rels; - Assert(list_length(stmt->tables) > 0); - - rels = OpenTableList(stmt->tables); - PublicationAddTables(puboid, rels, true, NULL); - CloseTableList(rels); + PublicationAddTables(puboid, stmt->tables, true, NULL); } table_close(rel, RowExclusiveLock); @@ -303,7 +307,7 @@ AlterPublicationOptions(AlterPublicationStmt *stmt, Relation rel, } else { - List *relids = GetPublicationRelations(pubform->oid); + List *relids = GetPublicationRelations(pubform->oid, true); /* * We don't want to send too many individual messages, at some point @@ -338,7 +342,6 @@ static void AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel, HeapTuple tup) { - List *rels = NIL; Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); Oid pubid = pubform->oid; @@ -352,15 +355,18 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel, Assert(list_length(stmt->tables) > 0); - rels = OpenTableList(stmt->tables); - if (stmt->tableAction == DEFELEM_ADD) - PublicationAddTables(pubid, rels, false, stmt); + PublicationAddTables(pubid, stmt->tables, false, stmt); else if (stmt->tableAction == DEFELEM_DROP) - PublicationDropTables(pubid, rels, false); + PublicationDropTables(pubid, stmt->tables, false); else /* DEFELEM_SET */ { - List *oldrelids = GetPublicationRelations(pubid); + /* + * Only fetch parent relations, because child relations cannot + * be dropped on their own as we might do based on the logic + * below. + */ + List *oldrelids = GetPublicationRelations(pubid, false); List *delrels = NIL; ListCell *oldlc; @@ -371,11 +377,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel, ListCell *newlc; bool found = false; - foreach(newlc, rels) + foreach(newlc, stmt->tables) { - Relation newrel = (Relation) lfirst(newlc); + RangeVar *newrel = (RangeVar *) lfirst(newlc); - if (RelationGetRelid(newrel) == oldrelid) + if (RangeVarGetRelid(newrel, + ShareUpdateExclusiveLock, + false) == oldrelid) { found = true; break; @@ -384,10 +392,10 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel, if (!found) { - Relation oldrel = table_open(oldrelid, - ShareUpdateExclusiveLock); + RangeVar *oldrelrv = makeRangeVar(get_namespace_name(get_rel_namespace(oldrelid)), + get_rel_name(oldrelid), -1); - delrels = lappend(delrels, oldrel); + delrels = lappend(delrels, oldrelrv); } } @@ -398,12 +406,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel, * Don't bother calculating the difference for adding, we'll catch and * skip existing ones when doing catalog update. */ - PublicationAddTables(pubid, rels, true, stmt); - - CloseTableList(delrels); + PublicationAddTables(pubid, stmt->tables, true, stmt); } - - CloseTableList(rels); } /* @@ -501,19 +505,17 @@ RemovePublicationRelById(Oid proid) } /* - * Open relations specified by a RangeVar list. - * The returned tables are locked in ShareUpdateExclusiveLock mode. + * Add listed tables to the publication. */ -static List * -OpenTableList(List *tables) +static void +PublicationAddTables(Oid pubid, List *tables, bool if_not_exists, + AlterPublicationStmt *stmt) { List *relids = NIL; - List *rels = NIL; ListCell *lc; - /* - * Open, share-lock, and check all the explicitly-specified relations - */ + Assert(!stmt || !stmt->for_all_tables); + foreach(lc, tables) { RangeVar *rv = castNode(RangeVar, lfirst(lc)); @@ -540,129 +542,221 @@ OpenTableList(List *tables) continue; } - rels = lappend(rels, rel); + /* Add to processed list. */ relids = lappend_oid(relids, myrelid); - /* Add children of this rel, if requested */ - if (recurse) - { - List *children; - ListCell *child; + PublicationAddTable(pubid, rel, if_not_exists, + stmt, recurse, false, &relids); - children = find_all_inheritors(myrelid, ShareUpdateExclusiveLock, - NULL); - - foreach(child, children) - { - Oid childrelid = lfirst_oid(child); - - /* Allow query cancel in case this takes a long time */ - CHECK_FOR_INTERRUPTS(); - - /* - * Skip duplicates if user specified both parent and child - * tables. - */ - if (list_member_oid(relids, childrelid)) - continue; - - /* find_all_inheritors already got lock */ - rel = table_open(childrelid, NoLock); - rels = lappend(rels, rel); - relids = lappend_oid(relids, childrelid); - } - } - } - - list_free(relids); - - return rels; -} - -/* - * Close all relations in the list. - */ -static void -CloseTableList(List *rels) -{ - ListCell *lc; - - foreach(lc, rels) - { - Relation rel = (Relation) lfirst(lc); - - table_close(rel, NoLock); + table_close(rel, ShareUpdateExclusiveLock); } } /* - * Add listed tables to the publication. + * Add given table and children (if any) to the publication. */ static void -PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, - AlterPublicationStmt *stmt) +PublicationAddTable(Oid pubid, Relation rel, bool if_not_exists, + AlterPublicationStmt *stmt, + bool recurse, bool recursing, + List **processed_relids) { - ListCell *lc; + ObjectAddress obj; + Oid relid = RelationGetRelid(rel); - Assert(!stmt || !stmt->for_all_tables); + /* Must be owner of the table or superuser. */ + if (!pg_class_ownercheck(RelationGetRelid(rel), GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind), + RelationGetRelationName(rel)); - foreach(lc, rels) + obj = publication_add_relation(pubid, rel, recursing, if_not_exists); + if (stmt) { - Relation rel = (Relation) lfirst(lc); - ObjectAddress obj; + EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress, + (Node *) stmt); - /* Must be owner of the table or superuser. */ - if (!pg_class_ownercheck(RelationGetRelid(rel), GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind), - RelationGetRelationName(rel)); + InvokeObjectPostCreateHook(PublicationRelRelationId, + obj.objectId, 0); + } - obj = publication_add_relation(pubid, rel, if_not_exists); - if (stmt) + /* Process children of this rel, if requested */ + if (recurse) + { + List *children; + ListCell *child; + + children = find_all_inheritors(relid, ShareUpdateExclusiveLock, + NULL); + + foreach(child, children) { - EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress, - (Node *) stmt); + Oid childrelid = lfirst_oid(child); - InvokeObjectPostCreateHook(PublicationRelRelationId, - obj.objectId, 0); + /* Allow query cancel in case this takes a long time */ + CHECK_FOR_INTERRUPTS(); + + /* + * Skip duplicates if user specified both parent and child + * tables. + */ + if (list_member_oid(*processed_relids, childrelid)) + continue; + + /* find_all_inheritors already got lock */ + rel = table_open(childrelid, NoLock); + + /* Add to processed list. */ + *processed_relids = lappend_oid(*processed_relids, childrelid); + + /* Recursively add this child to the publication. */ + PublicationAddTable(pubid, rel, if_not_exists, stmt, + recurse, true, processed_relids); + table_close(rel, NoLock); } } + else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && + rel->rd_partdesc->nparts > 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot add only partitioned table to publication when partitions exist"), + errhint("Do not specify the ONLY keyword."))); } /* * Remove listed tables from the publication. */ static void -PublicationDropTables(Oid pubid, List *rels, bool missing_ok) +PublicationDropTables(Oid pubid, List *tables, bool missing_ok) { - ObjectAddress obj; - ListCell *lc; - Oid prid; + ListCell *lc; + List *relids = NIL; - foreach(lc, rels) + foreach(lc, tables) { - Relation rel = (Relation) lfirst(lc); - Oid relid = RelationGetRelid(rel); + RangeVar *rv = castNode(RangeVar, lfirst(lc)); + bool recurse = rv->inh; + Relation rel; + Oid myrelid; - prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid, - ObjectIdGetDatum(relid), - ObjectIdGetDatum(pubid)); - if (!OidIsValid(prid)) + /* Allow query cancel in case this takes a long time */ + CHECK_FOR_INTERRUPTS(); + + rel = table_openrv(rv, ShareUpdateExclusiveLock); + myrelid = RelationGetRelid(rel); + + /* + * Filter out duplicates if user specifies "foo, foo". + * + * Note that this algorithm is known to not be very efficient (O(N^2)) + * but given that it only works on list of tables given to us by user + * it's deemed acceptable. + */ + if (list_member_oid(relids, myrelid)) { - if (missing_ok) - continue; - - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("relation \"%s\" is not part of the publication", - RelationGetRelationName(rel)))); + table_close(rel, ShareUpdateExclusiveLock); + continue; } - ObjectAddressSet(obj, PublicationRelRelationId, prid); - performDeletion(&obj, DROP_CASCADE, 0); + /* Add to processed list. */ + relids = lappend_oid(relids, myrelid); + + PublicationDropTable(pubid, rel, missing_ok, recurse, false, &relids); + + table_close(rel, ShareUpdateExclusiveLock); } } /* + * Remove given table and children (if any) from the publication. + */ +static void +PublicationDropTable(Oid pubid, Relation rel, bool missing_ok, + bool recurse, bool recursing, + List **processed_relids) +{ + ObjectAddress obj; + Oid relid = RelationGetRelid(rel); + Relation pubrelCatalog; + HeapTuple tuple; + Form_pg_publication_rel prform; + List *children; + ListCell *child; + + pubrelCatalog = table_open(PublicationRelRelationId, RowExclusiveLock); + tuple = SearchSysCache2(PUBLICATIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(pubid)); + if (!HeapTupleIsValid(tuple)) + { + if (missing_ok) + return; + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("relation \"%s\" is not part of the publication", + RelationGetRelationName(rel)))); + } + + prform = (Form_pg_publication_rel) GETSTRUCT(tuple); + + /* + * For a partition, check if we can really drop it from the + * publication. + */ + if (rel->rd_rel->relispartition && prform->prinh && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot drop partition \"%s\" from an inherited publication", + RelationGetRelationName(rel)), + errhint("Drop the parent from publication instead."))); + + ObjectAddressSet(obj, PublicationRelRelationId, prform->oid); + performDeletion(&obj, DROP_CASCADE, 0); + ReleaseSysCache(tuple); + + /* Process children of this rel, if requested */ + children = find_all_inheritors(relid, ShareUpdateExclusiveLock, + NULL); + + foreach(child, children) + { + Oid childrelid = lfirst_oid(child); + Relation childrel; + + /* Allow query cancel in case this takes a long time */ + CHECK_FOR_INTERRUPTS(); + + /* + * Skip duplicates if user specified both parent and child + * tables. + */ + if (list_member_oid(*processed_relids, childrelid)) + continue; + + /* find_all_inheritors already got lock */ + childrel = table_open(childrelid, NoLock); + + /* Add to processed list. */ + *processed_relids = lappend_oid(*processed_relids, childrelid); + + /* + * If requested, recursively drop this child from the publication. + * Otherwise, simply reset the inheritance flag of child's publication + * membership because the parent is no longer part of the publication. + */ + if (recurse) + PublicationDropTable(pubid, childrel, missing_ok, recurse, true, + processed_relids); + else + publication_rel_update_inheritance(pubrelCatalog, pubid, childrel, + false); + table_close(childrel, NoLock); + } + + table_close(pubrelCatalog, NoLock); +} + +/* * Internal workhorse for changing a publication owner */ static void @@ -772,3 +866,59 @@ AlterPublicationOwner_oid(Oid subid, Oid newOwnerId) table_close(rel, RowExclusiveLock); } + +/* + * This adds the partition and its sub-partitions (if any) to all of + * parent's publications. Partition's membership of those publications + * is attached to parent's membership, so for example, partition cannot + * be removed from the publication unless parent is also removed. + */ +void +ClonePublicationsForPartition(Relation parent, Relation partition) +{ + ListCell *lc; + List *parentPubs = GetRelationPublications(RelationGetRelid(parent)); + + /* Add partition and its sub-partitions (if any) to each publication. */ + foreach(lc, parentPubs) + { + Oid pubid = lfirst_oid(lc); + List *relids = list_make1_oid(RelationGetRelid(partition)); + + PublicationAddTable(pubid, partition, true, NULL, true, true, + &relids); + } +} + +/* + * This marks partition's membership in parent's publications as standalone, + * that is, not attached to the parent's membership in those publications. + * This function is called when detaching a partition from its parent. + */ +void +DetachPartitionPublications(Relation parent, Relation partition) +{ + ListCell *lc; + List *parentPubs = GetRelationPublications(RelationGetRelid(parent)); + Relation pubrelCatalog = NULL; + + if (parentPubs != NIL) + pubrelCatalog = table_open(PublicationRelRelationId, + RowExclusiveLock); + + /* + * For each publication, mark the partition's membership as no longer + * being inherited from parent. Note that we don't recurse to + * partition's own partitions. + */ + foreach(lc, parentPubs) + { + Oid pubid = lfirst_oid(lc); + + publication_rel_update_inheritance(pubrelCatalog, pubid, partition, + false); + } + + if (pubrelCatalog) + table_close(pubrelCatalog, NoLock); +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index ba8f4459f3..39cf0d40d5 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/policy.h" +#include "commands/publicationcmds.h" #include "commands/sequence.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1044,7 +1045,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, /* * If we're creating a partition, create now all the indexes, triggers, - * FKs defined in the parent. + * FKs defined in the parent. Also, add the partition to all + * publications that the parent is part of. * * We can't do it earlier, because DefineIndex wants to know the partition * key which we just stored. @@ -1117,6 +1119,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, */ CloneForeignKeyConstraints(NULL, parent, rel); + /* Add the partition to any publications that parent is part of. */ + ClonePublicationsForPartition(parent, rel); + table_close(parent, NoLock); } @@ -15673,6 +15678,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd) */ CloneForeignKeyConstraints(wqueue, rel, attachrel); + /* Add the partition to any publications that parent is part of. */ + ClonePublicationsForPartition(rel, attachrel); + /* * Generate partition constraint from the partition bound specification. * If the parent itself is a partition, make sure to include its @@ -16253,6 +16261,9 @@ ATExecDetachPartition(Relation rel, RangeVar *name) } CommandCounterIncrement(); + /* Mark partition's membership in parent's publications as local. */ + DetachPartitionPublications(rel, partRel); + /* * Invalidate the parent's relcache so that the partition is no longer * included in its partition descriptor. diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 95e027c970..f05f44c99f 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -591,17 +591,10 @@ CheckSubscriptionRelkind(char relkind, const char *nspname, const char *relname) { /* - * We currently only support writing to regular tables. However, give a - * more specific error for partitioned and foreign tables. + * We currently only support writing to regular and partitioned tables. + * However, give a more specific error for foreign tables. */ - if (relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot use relation \"%s.%s\" as logical replication target", - nspname, relname), - errdetail("\"%s.%s\" is a partitioned table.", - nspname, relname))); - else if (relkind == RELKIND_FOREIGN_TABLE) + if (relkind == RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot use relation \"%s.%s\" as logical replication target", @@ -609,7 +602,11 @@ CheckSubscriptionRelkind(char relkind, const char *nspname, errdetail("\"%s.%s\" is a foreign table.", nspname, relname))); - if (relkind != RELKIND_RELATION) + /* + * Subscription for partitioned tables are really placeholder objects, as + * replication itself occurs on the individual partition level. + */ + if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot use relation \"%s.%s\" as logical replication target", diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index 7881079e96..ed081743a9 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -646,7 +646,7 @@ fetch_remote_table_info(char *nspname, char *relname, WalRcvExecResult *res; StringInfoData cmd; TupleTableSlot *slot; - Oid tableRow[2] = {OIDOID, CHAROID}; + Oid tableRow[3] = {OIDOID, CHAROID, CHAROID}; Oid attrRow[4] = {TEXTOID, OIDOID, INT4OID, BOOLOID}; bool isnull; int natt; @@ -656,16 +656,16 @@ fetch_remote_table_info(char *nspname, char *relname, /* First fetch Oid and replica identity. */ initStringInfo(&cmd); - appendStringInfo(&cmd, "SELECT c.oid, c.relreplident" + appendStringInfo(&cmd, "SELECT c.oid, c.relreplident, c.relkind" " FROM pg_catalog.pg_class c" " INNER JOIN pg_catalog.pg_namespace n" " ON (c.relnamespace = n.oid)" " WHERE n.nspname = %s" " AND c.relname = %s" - " AND c.relkind = 'r'", + " AND pg_relation_is_publishable(c.oid)", quote_literal_cstr(nspname), quote_literal_cstr(relname)); - res = walrcv_exec(wrconn, cmd.data, 2, tableRow); + res = walrcv_exec(wrconn, cmd.data, 3, tableRow); if (res->status != WALRCV_OK_TUPLES) ereport(ERROR, @@ -682,6 +682,8 @@ fetch_remote_table_info(char *nspname, char *relname, Assert(!isnull); lrel->replident = DatumGetChar(slot_getattr(slot, 2, &isnull)); Assert(!isnull); + lrel->relkind = DatumGetChar(slot_getattr(slot, 3, &isnull)); + Assert(!isnull); ExecDropSingleTupleTableSlot(slot); walrcv_clear_result(res); @@ -769,6 +771,17 @@ copy_table(Relation rel) relmapentry = logicalrep_rel_open(lrel.remoteid, NoLock); Assert(rel == relmapentry->localrel); + /* + * Can't copy if either of the local and the remote relation is a + * partitioned table. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + lrel.relkind == RELKIND_PARTITIONED_TABLE) + { + logicalrep_rel_close(relmapentry, NoLock); + return; + } + /* Start copy on the publisher. */ initStringInfo(&cmd); appendStringInfo(&cmd, "COPY %s TO STDOUT", diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index f01fea5b91..5ef01faeed 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -3972,8 +3972,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) { TableInfo *tbinfo = &tblinfo[i]; - /* Only plain tables can be aded to publications. */ - if (tbinfo->relkind != RELKIND_RELATION) + /* Only plain and partitioned tables can be aded to publications. */ + if (tbinfo->relkind != RELKIND_RELATION && + tbinfo->relkind != RELKIND_PARTITIONED_TABLE) continue; /* @@ -3989,12 +3990,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) resetPQExpBuffer(query); - /* Get the publication membership for the table. */ + /* + * Get the publication membership for the table. Skip publications + * for which it has been added as a child via inheritance. + */ appendPQExpBuffer(query, "SELECT pr.tableoid, pr.oid, p.pubname " "FROM pg_publication_rel pr, pg_publication p " "WHERE pr.prrelid = '%u'" - " AND p.oid = pr.prpubid", + " AND p.oid = pr.prpubid" + " AND NOT prinh", tbinfo->dobj.catId.oid); res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); @@ -4053,7 +4058,12 @@ dumpPublicationTable(Archive *fout, PublicationRelInfo *pubrinfo) query = createPQExpBuffer(); - appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY", + /* For partitioned tables, recurse by default to add partitions. */ + if (pubrinfo->pubtable->relkind == RELKIND_PARTITIONED_TABLE) + appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE", + fmtId(pubrinfo->pubname)); + else + appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY", fmtId(pubrinfo->pubname)); appendPQExpBuffer(query, " %s;\n", fmtQualifiedDumpable(tbinfo)); diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h index 20a2f0ac1b..4a0b17806f 100644 --- a/src/include/catalog/pg_publication.h +++ b/src/include/catalog/pg_publication.h @@ -81,13 +81,15 @@ typedef struct Publication extern Publication *GetPublication(Oid pubid); extern Publication *GetPublicationByName(const char *pubname, bool missing_ok); extern List *GetRelationPublications(Oid relid); -extern List *GetPublicationRelations(Oid pubid); +extern List *GetPublicationRelations(Oid pubid, bool get_children); extern List *GetAllTablesPublications(void); extern List *GetAllTablesPublicationRelations(void); extern bool is_publishable_relation(Relation rel); extern ObjectAddress publication_add_relation(Oid pubid, Relation targetrel, - bool if_not_exists); + bool inh, bool if_not_exists); +void publication_rel_update_inheritance(Relation pubrelCatalog, Oid pubid, + Relation rel, bool inh); extern Oid get_publication_oid(const char *pubname, bool missing_ok); extern char *get_publication_name(Oid pubid, bool missing_ok); diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h index 5f5bc92ab3..46e5039a12 100644 --- a/src/include/catalog/pg_publication_rel.h +++ b/src/include/catalog/pg_publication_rel.h @@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId) Oid oid; /* oid */ Oid prpubid; /* Oid of the publication */ Oid prrelid; /* Oid of the relation */ + bool prinh; /* Is relation added due to inheritance? */ } FormData_pg_publication_rel; /* ---------------- diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h index c536b648f8..915b1e0056 100644 --- a/src/include/commands/publicationcmds.h +++ b/src/include/commands/publicationcmds.h @@ -25,5 +25,7 @@ extern void RemovePublicationRelById(Oid proid); extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId); extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId); +extern void ClonePublicationsForPartition(Relation parent, Relation partition); +extern void DetachPartitionPublications(Relation parent, Relation partition); #endif /* PUBLICATIONCMDS_H */ diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h index 3fc430af01..0fea368d99 100644 --- a/src/include/replication/logicalproto.h +++ b/src/include/replication/logicalproto.h @@ -45,6 +45,7 @@ typedef struct LogicalRepRelation LogicalRepRelId remoteid; /* unique id of the relation */ char *nspname; /* schema name */ char *relname; /* relation name */ + char relkind; /* relation kind */ int natts; /* number of columns */ char **attnames; /* column names */ Oid *atttyps; /* column types */ diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index feb51e4add..bf378782f8 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -47,7 +47,6 @@ CREATE SCHEMA pub_test; CREATE TABLE testpub_tbl1 (id serial primary key, data text); CREATE TABLE pub_test.testpub_nopk (foo int, bar int); CREATE VIEW testpub_view AS SELECT 1; -CREATE TABLE testpub_parted (a int) PARTITION BY LIST (a); SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub_foralltables FOR ALL TABLES WITH (publish = 'insert'); RESET client_min_messages; @@ -142,11 +141,151 @@ Tables: ALTER PUBLICATION testpub_default ADD TABLE testpub_view; ERROR: "testpub_view" is not a table DETAIL: Only tables can be added to publications. --- fail - partitioned table -ALTER PUBLICATION testpub_fortbl ADD TABLE testpub_parted; -ERROR: "testpub_parted" is a partitioned table -DETAIL: Adding partitioned tables to publications is not supported. -HINT: You can add the table partitions individually. +-- +-- Tests for partitioned tables +-- +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub_forparted; +RESET client_min_messages; +CREATE TABLE testpub_parted (a int) PARTITION BY LIST (a); +-- Can add "only" partitioned table when there are no partitions +ALTER PUBLICATION testpub_forparted ADD TABLE ONLY testpub_parted; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted; +CREATE TABLE testpub_parted1 partition of testpub_parted FOR VALUES IN (1) PARTITION BY LIST (a); +CREATE TABLE testpub_parted11 partition of testpub_parted1 FOR VALUES IN (1); +-- fail - cannot add "only" partitioned table +ALTER PUBLICATION testpub_forparted ADD TABLE ONLY testpub_parted; +ERROR: cannot add only partitioned table to publication when partitions exist +HINT: Do not specify the ONLY keyword. +-- ok, should add partition testpub_parted1, sub-partition testpub_parted11 +ALTER PUBLICATION testpub_forparted ADD TABLE testpub_parted; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted1" + "public.testpub_parted11" + +-- New partitions should automatically get added to the publications that +-- the parent is in. +CREATE TABLE testpub_parted2 PARTITION OF testpub_parted FOR VALUES IN (2); +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted1" + "public.testpub_parted11" + "public.testpub_parted2" + +-- Create a table that will later be attached to the parent and add it to +-- the same publication as the one that the parent is in +CREATE TABLE testpub_parted3 (LIKE testpub_parted); +ALTER PUBLICATION testpub_forparted ADD TABLE testpub_parted3; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted1" + "public.testpub_parted11" + "public.testpub_parted2" + "public.testpub_parted3" + +-- Attaching to parent should not result in the table being duplicatively +-- added to the publication, nor an error +ALTER TABLE testpub_parted ATTACH PARTITION testpub_parted3 FOR VALUES IN (3); +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted1" + "public.testpub_parted11" + "public.testpub_parted2" + "public.testpub_parted3" + +-- cannot drop a partition from publication which parent is still part of +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted1; +ERROR: cannot drop partition "testpub_parted1" from an inherited publication +HINT: Drop the parent from publication instead. +-- When the partition is detached, it and sub-partitions continue to be +-- members of the publication +ALTER TABLE testpub_parted DETACH PARTITION testpub_parted1; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted1" + "public.testpub_parted11" + "public.testpub_parted2" + "public.testpub_parted3" + +-- sub-partition's membership is still inherited, so can't drop +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted11; +ERROR: cannot drop partition "testpub_parted11" from an inherited publication +HINT: Drop the parent from publication instead. +-- dropping the now-detached partition should work though +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted1; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted2" + "public.testpub_parted3" + +-- Some tests for SET TABLE +-- no changes to the membership, because testpub_parted and testpub_parted3 +-- are already in the publication +ALTER PUBLICATION testpub_forparted SET TABLE testpub_parted, testpub_parted3; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted" + "public.testpub_parted2" + "public.testpub_parted3" + +-- This should drop testpub_parted (hence, testpub_parted2, testpub_parted3) +-- and add testpub_parted2, testpub_parted1 (hence testpub_parted11) +ALTER PUBLICATION testpub_forparted SET TABLE testpub_parted2, testpub_parted1; +\dRp+ testpub_forparted + Publication testpub_forparted + Owner | All tables | Inserts | Updates | Deletes | Truncates +--------------------------+------------+---------+---------+---------+----------- + regress_publication_user | f | t | t | t | t +Tables: + "public.testpub_parted1" + "public.testpub_parted11" + "public.testpub_parted2" + +DROP PUBLICATION testpub_forparted; +DROP TABLE testpub_parted, testpub_parted1; ALTER PUBLICATION testpub_default ADD TABLE testpub_tbl1; ALTER PUBLICATION testpub_default SET TABLE testpub_tbl1; ALTER PUBLICATION testpub_default ADD TABLE pub_test.testpub_nopk; @@ -219,7 +358,6 @@ ALTER PUBLICATION testpub2 ADD TABLE testpub_tbl1; -- ok DROP PUBLICATION testpub2; SET ROLE regress_publication_user; REVOKE CREATE ON DATABASE regression FROM regress_publication_user2; -DROP TABLE testpub_parted; DROP VIEW testpub_view; DROP TABLE testpub_tbl1; \dRp+ testpub_default diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 5773a755cf..06ece9e338 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -35,7 +35,6 @@ CREATE SCHEMA pub_test; CREATE TABLE testpub_tbl1 (id serial primary key, data text); CREATE TABLE pub_test.testpub_nopk (foo int, bar int); CREATE VIEW testpub_view AS SELECT 1; -CREATE TABLE testpub_parted (a int) PARTITION BY LIST (a); SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub_foralltables FOR ALL TABLES WITH (publish = 'insert'); @@ -83,8 +82,82 @@ CREATE PUBLICATION testpub_fortbl FOR TABLE testpub_tbl1; -- fail - view ALTER PUBLICATION testpub_default ADD TABLE testpub_view; --- fail - partitioned table -ALTER PUBLICATION testpub_fortbl ADD TABLE testpub_parted; + +-- +-- Tests for partitioned tables +-- + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub_forparted; +RESET client_min_messages; + +CREATE TABLE testpub_parted (a int) PARTITION BY LIST (a); + +-- Can add "only" partitioned table when there are no partitions +ALTER PUBLICATION testpub_forparted ADD TABLE ONLY testpub_parted; + +\dRp+ testpub_forparted + +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted; + +CREATE TABLE testpub_parted1 partition of testpub_parted FOR VALUES IN (1) PARTITION BY LIST (a); +CREATE TABLE testpub_parted11 partition of testpub_parted1 FOR VALUES IN (1); + +-- fail - cannot add "only" partitioned table +ALTER PUBLICATION testpub_forparted ADD TABLE ONLY testpub_parted; + +-- ok, should add partition testpub_parted1, sub-partition testpub_parted11 +ALTER PUBLICATION testpub_forparted ADD TABLE testpub_parted; +\dRp+ testpub_forparted + +-- New partitions should automatically get added to the publications that +-- the parent is in. +CREATE TABLE testpub_parted2 PARTITION OF testpub_parted FOR VALUES IN (2); +\dRp+ testpub_forparted + +-- Create a table that will later be attached to the parent and add it to +-- the same publication as the one that the parent is in +CREATE TABLE testpub_parted3 (LIKE testpub_parted); +ALTER PUBLICATION testpub_forparted ADD TABLE testpub_parted3; +\dRp+ testpub_forparted + +-- Attaching to parent should not result in the table being duplicatively +-- added to the publication, nor an error +ALTER TABLE testpub_parted ATTACH PARTITION testpub_parted3 FOR VALUES IN (3); +\dRp+ testpub_forparted + +-- cannot drop a partition from publication which parent is still part of +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted1; + +-- When the partition is detached, it and sub-partitions continue to be +-- members of the publication +ALTER TABLE testpub_parted DETACH PARTITION testpub_parted1; + +\dRp+ testpub_forparted + +-- sub-partition's membership is still inherited, so can't drop +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted11; + +-- dropping the now-detached partition should work though +ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted1; + +\dRp+ testpub_forparted + +-- Some tests for SET TABLE + +-- no changes to the membership, because testpub_parted and testpub_parted3 +-- are already in the publication +ALTER PUBLICATION testpub_forparted SET TABLE testpub_parted, testpub_parted3; + +\dRp+ testpub_forparted + +-- This should drop testpub_parted (hence, testpub_parted2, testpub_parted3) +-- and add testpub_parted2, testpub_parted1 (hence testpub_parted11) +ALTER PUBLICATION testpub_forparted SET TABLE testpub_parted2, testpub_parted1; +\dRp+ testpub_forparted + +DROP PUBLICATION testpub_forparted; +DROP TABLE testpub_parted, testpub_parted1; ALTER PUBLICATION testpub_default ADD TABLE testpub_tbl1; ALTER PUBLICATION testpub_default SET TABLE testpub_tbl1; @@ -125,7 +198,6 @@ DROP PUBLICATION testpub2; SET ROLE regress_publication_user; REVOKE CREATE ON DATABASE regression FROM regress_publication_user2; -DROP TABLE testpub_parted; DROP VIEW testpub_view; DROP TABLE testpub_tbl1; -- 2.11.0