From 110244aa38bc27da051c0b13ee3a79d689ccaa2c Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
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 @@
 
    <listitem>
     <para>
-     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.
+    </para>
+
+    <para>
+     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.
     </para>
    </listitem>
   </itemizedlist>
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 <replaceable class="parameter">name</replaceable> RENAME TO <r
    tables from the publication.  Note that adding tables to a publication that
    is already subscribed to will require a <literal>ALTER SUBSCRIPTION
    ... REFRESH PUBLICATION</literal> action on the subscribing side in order
-   to become effective.
+   to become effective.  Using <literal>DROP TABLE</literal> to remove a
+   partitioned table from a publication will also remove all of its partitions
+   from the publication unless <literal>ONLY</literal> is specified.  However,
+   removing a partition from a publication without first removing its parent
+   will result in an error.
   </para>
 
   <para>
@@ -91,7 +95,10 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> 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
+      <literal>ONLY</literal> with <literal>SET TABLE</literal> will result in an
+      error for a partitioned table if it contains partitions, because partitions
+      must be added to the publication too.
      </para>
     </listitem>
    </varlistentry>
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 <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      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
+      <literal>ONLY</literal> results in an error for a partitioned table if
+      it contains partitions, because partitions must be added to the
+      publication too.  See <xref linkend="logical-replication-publication"/>
+      for details about how partitioned tables are replicated.
      </para>
     </listitem>
    </varlistentry>
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

