Subject: Re: Column Filtering in Logical Replication

From: Peter Eisentraut <[email-masked]>
Date: Mon, 31 Jan 2022 08:13:47 +0100

On 12.01.22 01:41, Alvaro Herrera wrote:
> I discovered a big hole in this, which is that ALTER PUBLICATION SET
> (publish='insert,update') can add UPDATE publishing to a publication
> that was only publishing INSERTs.  It's easy to implement a fix: in
> AlterPublicationOptions, scan the list of tables and raise an error if
> any of them has a column list that doesn't include all the columns in
> the replica identity.

Right now, we are not checking the publication options and the replica 
identity combinations at all at DDL time.  This is only checked at 
execution time in CheckCmdReplicaIdentity().  So under that scheme I 
don't think the check you describe is actually necessary.  Let the user 
set whatever combination they want, and check at execution time if it's 
an UPDATE or DELETE command whether the replica identity is sufficient.

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 16 Feb 2022 00:33:37 +0100

Hi,

Here's an updated version of the patch, rebased to current master. Parts 
0002 and 0003 include various improvements based on review by me and 
another one by Peter Smith [1].

Part 0003 reworks and significantly extends the TAP test, to exercise 
various cases related to changes of replica identity etc. discussed in 
this thread. Some of the tests however still fail, because the behavior 
was not updated - I'll work on that once we agree what the expected 
behavior is.

1) partitioning with pubviaroot=true

The main set of failures is related to partitions with different replica 
identities and (pubviaroot=true), some of which may be mismatching the 
column list. There are multiple such test cases, depending on how the 
inconsistency is introduced - it may be there from the beginning, the 
column filter may be modified after adding the partitioned table to the 
publication, etc.

I think the expected behavior is to prohibit such cases from happening, 
by cross-checking the column filter when adding the partitioned table to 
publication, attaching a partition or changing a column filter.

2) merging multiple column filters

When the table has multiple column filters (in different publications), 
we need to merge them. Which works, except that FOR ALL TABLES [IN 
SCHEMA] needs to be handled as "has no column filter" (and replicates 
everything).

3) partitioning with pubivaroot=false

When a partitioned table is added with (pubviaroot=false), it should not 
be subject to column filter on the parent relation, which is the same 
behavior used by the row filtering patch.

regards

[1] 
https://www.postgresql.org/message-id/CAHut%2BPtc7Rh187eQKrxdUmUNWyfxz7OkhYAX%3DAW411Qwxya0LQ%40mail.gmail.com

-- 
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

From 1d5951c26a2821d20c995978eaa4028a39f56565 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Mon, 14 Feb 2022 17:13:04 +0100
Subject: [PATCH 1/4] Support column lists for logical replication of tables

Add the capability of specifying a column list for individual tables as
part of a publication.  Columns not in the list are not published.  This
enables replicating to a table with only a subset of the columns.

If no column list is specified, all the columns are replicated, as
previously

Author: Rahila Syed <[email-masked]>
Discussion: https://postgr.es/m/CAH2L28vddB_NFdRVpuyRBJEBWjz4BSyTB=[email-masked]
---
 doc/src/sgml/catalogs.sgml                  |  13 +
 doc/src/sgml/protocol.sgml                  |   4 +-
 doc/src/sgml/ref/alter_publication.sgml     |  20 +-
 doc/src/sgml/ref/create_publication.sgml    |  11 +-
 src/backend/catalog/pg_publication.c        | 353 +++++++++++++++++++-
 src/backend/commands/publicationcmds.c      |  67 +++-
 src/backend/commands/tablecmds.c            |  87 ++++-
 src/backend/nodes/copyfuncs.c               |   1 +
 src/backend/nodes/equalfuncs.c              |   1 +
 src/backend/parser/gram.y                   |  60 +++-
 src/backend/replication/logical/proto.c     |  60 ++--
 src/backend/replication/logical/tablesync.c | 119 ++++++-
 src/backend/replication/pgoutput/pgoutput.c | 118 ++++++-
 src/bin/pg_dump/pg_dump.c                   |  41 ++-
 src/bin/pg_dump/pg_dump.h                   |   1 +
 src/bin/psql/describe.c                     |  26 +-
 src/include/catalog/pg_publication.h        |   6 +
 src/include/catalog/pg_publication_rel.h    |   3 +
 src/include/nodes/parsenodes.h              |   4 +-
 src/include/replication/logicalproto.h      |   6 +-
 src/test/regress/expected/publication.out   |  59 +++-
 src/test/regress/sql/publication.sql        |  39 ++-
 src/test/subscription/t/028_column_list.pl  | 164 +++++++++
 23 files changed, 1179 insertions(+), 84 deletions(-)
 create mode 100644 src/test/subscription/t/028_column_list.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5a1627a3941..f20cb2b78ab 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6325,6 +6325,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        Reference to relation
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>prattrs</structfield> <type>int2vector</type>
+       (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+      </para>
+      <para>
+       This is an array of values that indicates which table columns are
+       part of the publication.  For example a value of <literal>1 3</literal>
+       would mean that the first and the third table columns are published.
+       A null value indicates that all attributes are published.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 1c5ab008791..91541cd8cf7 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -7005,7 +7005,9 @@ Relation
 </listitem>
 </varlistentry>
 </variablelist>
-        Next, the following message part appears for each column (except generated columns):
+        Next, the following message part appears for each column (except
+        generated columns and other columns that don't appear in the column
+        filter list, for tables that have one):
 <variablelist>
 <varlistentry>
 <term>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 7c7c27bf7ce..a85574214a5 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -25,12 +25,13 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replace
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET <replaceable class="parameter">publication_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replaceable class="parameter">publication_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ALTER TABLE <replaceable class="parameter">publication_object</replaceable> SET COLUMNS { ( <replaceable class="parameter">name</replaceable> [, ...] ) | ALL }
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <replaceable>new_name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable>, [, ... ] ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -62,6 +63,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    command retain their previous settings.
   </para>
 
+  <para>
+   The <literal>ALTER TABLE ... SET COLUMNS</literal> variant allows changing
+   the set of columns that are included in the publication.
+  </para>
+
   <para>
    The remaining variants change the owner and the name of the publication.
   </para>
@@ -110,6 +116,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
       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.
+      Optionally, a column list can be specified.  See <xref
+      linkend="sql-createpublication"/> for details.
      </para>
     </listitem>
    </varlistentry>
@@ -164,9 +172,15 @@ ALTER PUBLICATION noinsert SET (publish = 'update, delete');
   </para>
 
   <para>
-   Add some tables to the publication:
+   Add tables to the publication:
+<programlisting>
+ALTER PUBLICATION mypublication ADD TABLE users (user_id, firstname), departments;
+</programlisting></para>
+
+  <para>
+   Change the set of columns published for a table:
 <programlisting>
-ALTER PUBLICATION mypublication ADD TABLE users, departments;
+ALTER PUBLICATION mypublication ALTER TABLE users SET COLUMNS (user_id, firstname, lastname);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 385975bfadd..a59cd3f532a 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable>, [, ... ] ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -78,6 +78,15 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
       publication, so they are never explicitly added to the publication.
      </para>
 
+     <para>
+      When a column list is specified, only the listed columns are replicated;
+      any other columns are ignored for the purpose of replication through
+      this publication.  If no column list is specified, all columns of the
+      table are replicated through this publication, including any columns
+      added later.  If a column list is specified, it must include the replica
+      identity columns.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index e14ca2f5630..aa1655696f5 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -45,13 +45,26 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+
+static void check_publication_columns(Publication *pub, Relation targetrel,
+									  Bitmapset *columns);
+static void publication_translate_columns(Relation targetrel, List *columns,
+										  int *natts, AttrNumber **attrs,
+										  Bitmapset **attset);
+
 /*
- * Check if relation can be in given publication and throws appropriate
- * error if not.
+ * Check if relation can be in given publication and that the column
+ * filter is sensible, and throws appropriate error if not.
+ *
+ * targetcols is the bitmapset of attribute numbers given in the column list,
+ * or NULL if it was not specified.
  */
 static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Publication *pub, Relation targetrel,
+							   Bitmapset *columns)
 {
+	bool		replidentfull = (targetrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
+
 	/* Must be a regular or partitioned table */
 	if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
 		RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
@@ -82,6 +95,73 @@ check_publication_add_relation(Relation targetrel)
 				 errmsg("cannot add relation \"%s\" to publication",
 						RelationGetRelationName(targetrel)),
 				 errdetail("This operation is not supported for unlogged tables.")));
+
+	/* Make sure the column list checks out */
+	if (columns != NULL)
+	{
+		/*
+		 * Even if the user listed all columns in the column list, we cannot
+		 * allow a column list to be specified when REPLICA IDENTITY is FULL;
+		 * that would cause problems if a new column is added later, because
+		 * the new column would have to be included (because of being part of
+		 * the replica identity) but it's technically not allowed (because of
+		 * not being in the publication's column list yet).  So reject this
+		 * case altogether.
+		 */
+		if (replidentfull)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("invalid column list for publishing relation \"%s\"",
+						   RelationGetRelationName(targetrel)),
+					errdetail("Cannot specify a column list on relations with REPLICA IDENTITY FULL."));
+
+		check_publication_columns(pub, targetrel, columns);
+	}
+}
+
+/*
+ * Enforce that the column list can only leave out columns that aren't
+ * forced to be sent.
+ *
+ * No column can be excluded if REPLICA IDENTITY is FULL (since all the
+ * columns need to be sent regardless); and in other cases, the columns in
+ * the REPLICA IDENTITY cannot be left out.
+ */
+static void
+check_publication_columns(Publication *pub, Relation targetrel, Bitmapset *columns)
+{
+	if (targetrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot change column set for relation \"%s\"",
+					   RelationGetRelationName(targetrel)),
+				errdetail("Cannot specify column list on relations with REPLICA IDENTITY FULL."));
+
+	if (pub->pubactions.pubupdate || pub->pubactions.pubdelete)
+	{
+		Bitmapset  *idattrs;
+		int			x;
+
+		idattrs = RelationGetIndexAttrBitmap(targetrel,
+											 INDEX_ATTR_BITMAP_IDENTITY_KEY);
+
+		/*
+		 * We have to test membership the hard way, because the values returned by
+		 * RelationGetIndexAttrBitmap are offset.
+		 */
+		x = -1;
+		while ((x = bms_next_member(idattrs, x)) >= 0)
+		{
+			if (!bms_is_member(x + FirstLowInvalidHeapAttributeNumber, columns))
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+						errmsg("invalid column list for publishing relation \"%s\"",
+							   RelationGetRelationName(targetrel)),
+						errdetail("All columns in REPLICA IDENTITY must be present in the column list."));
+		}
+
+		bms_free(idattrs);
+	}
 }
 
 /*
@@ -289,6 +369,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	Oid			relid = RelationGetRelid(targetrel->relation);
 	Oid			pubreloid;
 	Publication *pub = GetPublication(pubid);
+	Bitmapset  *attset = NULL;
+	AttrNumber *attarray;
+	int			natts = 0;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
@@ -314,7 +397,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 						RelationGetRelationName(targetrel->relation), pub->name)));
 	}
 
-	check_publication_add_relation(targetrel->relation);
+	/* Translate column names to numbers and verify suitability */
+	publication_translate_columns(targetrel->relation,
+								  targetrel->columns,
+								  &natts, &attarray, &attset);
+
+	check_publication_add_relation(pub, targetrel->relation, attset);
+
+	bms_free(attset);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
@@ -327,6 +417,15 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	if (targetrel->columns)
+	{
+		int2vector *prattrs;
+
+		prattrs = buildint2vector(attarray, natts);
+		values[Anum_pg_publication_rel_prattrs - 1] = PointerGetDatum(prattrs);
+	}
+	else
+		nulls[Anum_pg_publication_rel_prattrs - 1] = true;
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -337,6 +436,13 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	/* Register dependencies as needed */
 	ObjectAddressSet(myself, PublicationRelRelationId, pubreloid);
 
+	/* Add dependency on the columns, if any are listed */
+	for (int i = 0; i < natts; i++)
+	{
+		ObjectAddressSubSet(referenced, RelationRelationId, relid, attarray[i]);
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+	}
+	pfree(attarray);
 	/* Add dependency on the publication */
 	ObjectAddressSet(referenced, PublicationRelationId, pubid);
 	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
@@ -364,6 +470,155 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	return myself;
 }
 
+/*
+ * Update the column list for a relation in a publication.
+ */
+void
+publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
+							  Relation targetrel, List *columns)
+{
+	Bitmapset  *attset;
+	AttrNumber *attarray;
+	HeapTuple	copytup;
+	int			natts;
+	bool		nulls[Natts_pg_publication_rel];
+	bool		replaces[Natts_pg_publication_rel];
+	Datum		values[Natts_pg_publication_rel];
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	replaces[Anum_pg_publication_rel_prattrs - 1] = true;
+
+	deleteDependencyRecordsForClass(PublicationRelationId,
+									((Form_pg_publication_rel) GETSTRUCT(pubreltup))->oid,
+									RelationRelationId,
+									DEPENDENCY_AUTO);
+
+	if (columns == NULL)
+	{
+		nulls[Anum_pg_publication_rel_prattrs - 1] = true;
+	}
+	else
+	{
+		ObjectAddress myself,
+					referenced;
+		int2vector *prattrs;
+		Publication *pub;
+
+		pub = GetPublication(((Form_pg_publication_rel) GETSTRUCT(pubreltup))->prpubid);
+
+		publication_translate_columns(targetrel, columns,
+									  &natts, &attarray, &attset);
+
+		/*
+		 * Make sure the column list checks out.  XXX this should occur at
+		 * caller in publicationcmds.c, not here.
+		 */
+		check_publication_columns(pub, targetrel, attset);
+		bms_free(attset);
+		/* XXX "pub" is leaked here */
+
+		prattrs = buildint2vector(attarray, natts);
+		values[Anum_pg_publication_rel_prattrs - 1] = PointerGetDatum(prattrs);
+
+		/* Add dependencies on the new list of columns */
+		ObjectAddressSet(myself, PublicationRelRelationId,
+						 ((Form_pg_publication_rel) GETSTRUCT(pubreltup))->oid);
+		for (int i = 0; i < natts; i++)
+		{
+			ObjectAddressSubSet(referenced, RelationRelationId,
+								RelationGetRelid(targetrel), attarray[i]);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+		}
+	}
+
+	copytup = heap_modify_tuple(pubreltup, RelationGetDescr(pubrel),
+								values, nulls, replaces);
+
+	CatalogTupleUpdate(pubrel, &pubreltup->t_self, copytup);
+
+	heap_freetuple(copytup);
+}
+
+/* qsort comparator for attnums */
+static int
+compare_int16(const void *a, const void *b)
+{
+	int			av = *(const int16 *) a;
+	int			bv = *(const int16 *) b;
+
+	/* this can't overflow if int is wider than int16 */
+	return (av - bv);
+}
+
+/*
+ * Translate a list of column names to an array of attribute numbers
+ * and a Bitmapset with them; verify that each attribute is appropriate
+ * to have in a publication column list.  Other checks are done later;
+ * see check_publication_columns.
+ *
+ * Note that the attribute numbers are *not* offset by
+ * FirstLowInvalidHeapAttributeNumber; system columns are forbidden so this
+ * is okay.
+ */
+static void
+publication_translate_columns(Relation targetrel, List *columns, int *natts,
+							  AttrNumber **attrs, Bitmapset **attset)
+{
+	AttrNumber *attarray;
+	Bitmapset  *set = NULL;
+	ListCell   *lc;
+	int			n = 0;
+	TupleDesc	tupdesc = RelationGetDescr(targetrel);
+
+	/*
+	 * Translate list of columns to attnums. We prohibit system attributes and
+	 * make sure there are no duplicate columns.
+	 */
+	attarray = palloc(sizeof(AttrNumber) * list_length(columns));
+	foreach(lc, columns)
+	{
+		char	   *colname = strVal(lfirst(lc));
+		AttrNumber	attnum = get_attnum(RelationGetRelid(targetrel), colname);
+
+		if (attnum == InvalidAttrNumber)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+						   colname, RelationGetRelationName(targetrel)));
+
+		if (!AttrNumberIsForUserDefinedAttr(attnum))
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot reference system column \"%s\" in publication column list",
+						   colname));
+
+		if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot reference generated column \"%s\" in publication column list",
+						   colname));
+
+		if (bms_is_member(attnum, set))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("duplicate column \"%s\" in publication column list",
+						   colname));
+
+		set = bms_add_member(set, attnum);
+		attarray[n++] = attnum;
+	}
+
+	/* Be tidy, so that the catalog representation is always sorted */
+	qsort(attarray, n, sizeof(AttrNumber), compare_int16);
+
+	*natts = n;
+	*attrs = attarray;
+	*attset = set;
+}
+
 /*
  * Insert new publication / schema mapping.
  */
@@ -471,6 +726,96 @@ GetRelationPublications(Oid relid)
 	return result;
 }
 
+/*
+ * Gets a list of OIDs of all partial-column publications of the given
+ * relation, that is, those that specify a column list.
+ */
+List *
+GetRelationColumnPartialPublications(Oid relid)
+{
+	CatCList   *pubrellist;
+	List	   *pubs = NIL;
+
+	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
+									 ObjectIdGetDatum(relid));
+	for (int i = 0; i < pubrellist->n_members; i++)
+	{
+		HeapTuple	tup = &pubrellist->members[i]->tuple;
+		bool		isnull;
+
+		(void) SysCacheGetAttr(PUBLICATIONRELMAP, tup,
+							   Anum_pg_publication_rel_prattrs,
+							   &isnull);
+		if (isnull)
+			continue;
+
+		pubs = lappend_oid(pubs,
+						   ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid);
+	}
+
+	ReleaseSysCacheList(pubrellist);
+
+	return pubs;
+}
+
+/* FIXME maybe these two routines should be in lsyscache.c */
+/* Return the set of actions that the given publication includes */
+void
+GetActionsInPublication(Oid pubid, PublicationActions *actions)
+{
+	HeapTuple	pub;
+	Form_pg_publication	pubForm;
+
+	pub = SearchSysCache1(PUBLICATIONOID,
+						  ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(pub))
+		elog(ERROR, "cache lookup failed for publication %u", pubid);
+
+	pubForm = (Form_pg_publication) GETSTRUCT(pub);
+	actions->pubinsert = pubForm->pubinsert;
+	actions->pubupdate = pubForm->pubupdate;
+	actions->pubdelete = pubForm->pubdelete;
+	actions->pubtruncate = pubForm->pubtruncate;
+
+	ReleaseSysCache(pub);
+}
+
+/*
+ * For a relation in a publication that is known to have a non-null column
+ * list, return the list of attribute numbers that are in it.
+ */
+List *
+GetRelationColumnListInPublication(Oid relid, Oid pubid)
+{
+	HeapTuple	tup;
+	Datum		adatum;
+	bool		isnull;
+	ArrayType  *arr;
+	int			nelems;
+	int16	   *elems;
+	List	   *attnos = NIL;
+
+	tup = SearchSysCache2(PUBLICATIONRELMAP,
+						  ObjectIdGetDatum(relid),
+						  ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for rel %u of publication %u", relid, pubid);
+	adatum = SysCacheGetAttr(PUBLICATIONRELMAP, tup,
+							 Anum_pg_publication_rel_prattrs, &isnull);
+	if (isnull)
+		elog(ERROR, "found unexpected null in pg_publication_rel.prattrs");
+	arr = DatumGetArrayTypeP(adatum);
+	nelems = ARR_DIMS(arr)[0];
+	elems = (int16 *) ARR_DATA_PTR(arr);
+
+	for (int i = 0; i < nelems; i++)
+		attnos = lappend_oid(attnos, elems[i]);
+
+	ReleaseSysCache(tup);
+
+	return attnos;
+}
+
 /*
  * Gets list of relation oids for a publication.
  *
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0e4bb97fb73..592f56dd61e 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -376,6 +376,46 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	return myself;
 }
 
+/*
+ * Change the column list of a relation in a publication
+ */
+static void
+PublicationSetColumns(AlterPublicationStmt *stmt,
+					  Form_pg_publication pubform, PublicationTable *table)
+{
+	Relation	rel,
+				urel;
+	HeapTuple	tup;
+	ObjectAddress obj,
+				secondary;
+
+	rel = table_open(PublicationRelRelationId, RowExclusiveLock);
+	urel = table_openrv(table->relation, ShareUpdateExclusiveLock);
+
+	tup = SearchSysCache2(PUBLICATIONRELMAP,
+						  ObjectIdGetDatum(RelationGetRelid(urel)),
+						  ObjectIdGetDatum(pubform->oid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errmsg("relation \"%s\" is not already in publication \"%s\"",
+					   table->relation->relname,
+					   NameStr(pubform->pubname)));
+
+	publication_set_table_columns(rel, tup, urel, table->columns);
+
+	ObjectAddressSet(obj, PublicationRelationId,
+					 ((Form_pg_publication_rel) GETSTRUCT(tup))->oid);
+	ObjectAddressSet(secondary, RelationRelationId, RelationGetRelid(urel));
+	EventTriggerCollectSimpleCommand(obj, secondary, (Node *) stmt);
+
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+	table_close(urel, NoLock);
+
+	InvokeObjectPostAlterHook(PublicationRelationId, pubform->oid, 0);
+}
+
 /*
  * Change options of a publication.
  */
@@ -523,6 +563,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	}
 	else if (stmt->action == AP_DropObjects)
 		PublicationDropTables(pubid, rels, false);
+	else if (stmt->action == AP_SetColumns)
+	{
+		Assert(schemaidlist == NIL);
+		Assert(list_length(tables) == 1);
+
+		PublicationSetColumns(stmt, pubform,
+							  linitial_node(PublicationTable, tables));
+	}
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
@@ -562,7 +610,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 
 				pubrel = palloc(sizeof(PublicationRelInfo));
 				pubrel->relation = oldrel;
-
+				/* This is not needed to delete a table */
+				pubrel->columns = NIL;
 				delrels = lappend(delrels, pubrel);
 			}
 		}
@@ -622,7 +671,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 	}
 	else if (stmt->action == AP_DropObjects)
 		PublicationDropSchemas(pubform->oid, schemaidlist, false);
-	else						/* AP_SetObjects */
+	else if (stmt->action == AP_SetObjects)
 	{
 		List	   *oldschemaids = GetPublicationSchemas(pubform->oid);
 		List	   *delschemas = NIL;
@@ -645,6 +694,10 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		 */
 		PublicationAddSchemas(pubform->oid, schemaidlist, true, stmt);
 	}
+	else
+	{
+		/* Nothing to do for AP_SetColumns */
+	}
 }
 
 /*
@@ -934,6 +987,8 @@ OpenTableList(List *tables)
 
 		pub_rel = palloc(sizeof(PublicationRelInfo));
 		pub_rel->relation = rel;
+		pub_rel->columns = t->columns;
+
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -967,8 +1022,11 @@ OpenTableList(List *tables)
 
 				/* find_all_inheritors already got lock */
 				rel = table_open(childrelid, NoLock);
+
 				pub_rel = palloc(sizeof(PublicationRelInfo));
 				pub_rel->relation = rel;
+				pub_rel->columns = t->columns;
+
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 			}
@@ -1076,6 +1134,11 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 		Relation	rel = pubrel->relation;
 		Oid			relid = RelationGetRelid(rel);
 
+		if (pubrel->columns)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("column list must not be specified in ALTER PUBLICATION ... DROP"));
+
 		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
 							   ObjectIdGetDatum(relid),
 							   ObjectIdGetDatum(pubid));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3e83f375b55..28de9329800 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8365,6 +8365,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 				 bool missing_ok, LOCKMODE lockmode,
 				 ObjectAddresses *addrs)
 {
+	Oid			relid = RelationGetRelid(rel);
 	HeapTuple	tuple;
 	Form_pg_attribute targetatt;
 	AttrNumber	attnum;
@@ -8384,7 +8385,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 	/*
 	 * get the number of the attribute
 	 */
-	tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+	tuple = SearchSysCacheAttName(relid, colName);
 	if (!HeapTupleIsValid(tuple))
 	{
 		if (!missing_ok)
@@ -8438,13 +8439,42 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	/*
+	 * Also, if the column is used in the column list of a publication,
+	 * disallow the drop if the DROP is RESTRICT.  We don't do anything if the
+	 * DROP is CASCADE, which means that the dependency mechanism will remove
+	 * the relation from the publication.
+	 */
+	if (behavior == DROP_RESTRICT)
+	{
+		List	   *pubs;
+		ListCell   *lc;
+
+		pubs = GetRelationColumnPartialPublications(relid);
+		foreach(lc, pubs)
+		{
+			Oid			pubid = lfirst_oid(lc);
+			List	   *published_cols;
+
+			published_cols =
+				GetRelationColumnListInPublication(relid, pubid);
+
+			if (list_member_oid(published_cols, attnum))
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop column \"%s\" because it is part of publication \"%s\"",
+							   colName, get_publication_name(pubid, false)),
+						errhint("Specify CASCADE or use ALTER PUBLICATION to remove the column from the publication."));
+		}
+	}
+
 	/*
 	 * Propagate to children as appropriate.  Unlike most other ALTER
 	 * routines, we have to do this one level of recursion at a time; we can't
 	 * use find_all_inheritors to do it in one pass.
 	 */
 	children =
-		find_inheritance_children(RelationGetRelid(rel), lockmode);
+		find_inheritance_children(relid, lockmode);
 
 	if (children)
 	{
@@ -8532,7 +8562,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	/* Add object to delete */
 	object.classId = RelationRelationId;
-	object.objectId = RelationGetRelid(rel);
+	object.objectId = relid;
 	object.objectSubId = attnum;
 	add_exact_object_address(&object, addrs);
 
@@ -15842,6 +15872,7 @@ relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid,
 			CatalogTupleUpdate(pg_index, &pg_index_tuple->t_self, pg_index_tuple);
 			InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0,
 										 InvalidOid, is_internal);
+
 			/*
 			 * Invalidate the relcache for the table, so that after we commit
 			 * all sessions will refresh the table's replica identity index
@@ -15864,6 +15895,11 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	Oid			indexOid;
 	Relation	indexRel;
 	int			key;
+	List	   *pubs;
+	Bitmapset  *indexed_cols = NULL;
+	ListCell   *lc;
+
+	pubs = GetRelationColumnPartialPublications(RelationGetRelid(rel));
 
 	if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT)
 	{
@@ -15872,11 +15908,16 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_FULL)
 	{
+		if (pubs != NIL)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot set REPLICA IDENTITY FULL when publications contain relations that specify column lists"));
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
 		return;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_NOTHING)
 	{
+		/* XXX not sure what's the right check for publications here */
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
 		return;
 	}
@@ -15961,6 +16002,46 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 					 errmsg("index \"%s\" cannot be used as replica identity because column \"%s\" is nullable",
 							RelationGetRelationName(indexRel),
 							NameStr(attr->attname))));
+
+		/*
+		 * Collect columns used, in case we have any publications that we need
+		 * to vet.  System attributes are disallowed so no need to subtract
+		 * FirstLowInvalidHeapAttributeNumber.
+		 */
+		indexed_cols = bms_add_member(indexed_cols, attno);
+	}
+
+	/*
+	 * Check partial-column publications.  For those that include UPDATE and
+	 * DELETE, we must enforce that the columns in the replica identity are
+	 * included in the column list.  For publications that only include INSERT
+	 * and TRUNCATE, we don't need to restrict the replica identity.
+	 */
+	foreach(lc, pubs)
+	{
+		Oid			pubid = lfirst_oid(lc);
+		List	   *published_cols;
+		PublicationActions actions;
+
+		GetActionsInPublication(pubid, &actions);
+		/* No need to worry about this one */
+		if (!actions.pubupdate && !actions.pubdelete)
+			continue;
+
+		published_cols =
+			GetRelationColumnListInPublication(RelationGetRelid(rel), pubid);
+
+		for (key = 0; key < IndexRelationGetNumberOfKeyAttributes(indexRel); key++)
+		{
+			int16	attno = indexRel->rd_index->indkey.values[key];
+
+			if (!list_member_oid(published_cols, attno))
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("index \"%s\" cannot be used because publication \"%s\" does not include all indexed columns",
+							   RelationGetRelationName(indexRel),
+							   get_publication_name(pubid, false)));
+		}
 	}
 
 	/* This index is suitable for use as a replica identity. Mark it. */
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bc0d90b4b1b..58603ab18e1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4849,6 +4849,7 @@ _copyPublicationTable(const PublicationTable *from)
 	PublicationTable *newnode = makeNode(PublicationTable);
 
 	COPY_NODE_FIELD(relation);
+	COPY_NODE_FIELD(columns);
 
 	return newnode;
 }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 2e7122ad2f6..f7f1b8be2ed 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2321,6 +2321,7 @@ static bool
 _equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
 {
 	COMPARE_NODE_FIELD(relation);
+	COMPARE_NODE_FIELD(columns);
 
 	return true;
 }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 92f93cfc72d..3e7455dd5e9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9751,12 +9751,13 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr
+			TABLE relation_expr opt_column_list
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
+					$$->pubtable->columns = $3;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -9771,28 +9772,38 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @5;
 				}
-			| ColId
+			| ColId opt_column_list
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
-					$$->name = $1;
+					if ($2 != NULL)
+					{
+						$$->pubtable = makeNode(PublicationTable);
+						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
+						$$->pubtable->columns = $2;
+						$$->name = NULL;
+					}
+					else
+						$$->name = $1;
 					$$->location = @1;
 				}
-			| ColId indirection
+			| ColId indirection opt_column_list
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
+					$$->pubtable->columns = $3;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr
+			| extended_relation_expr opt_column_list
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
+					$$->pubtable->columns = $2;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -9818,6 +9829,9 @@ pub_obj_list: 	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name SET COLUMNS table_name (column[, ...])
+ * ALTER PUBLICATION name SET COLUMNS table_name ALL
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -9851,6 +9865,32 @@ AlterPublicationStmt:
 					n->action = AP_SetObjects;
 					$$ = (Node *)n;
 				}
+			| ALTER PUBLICATION name ALTER TABLE relation_expr SET COLUMNS '(' columnList ')'
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					PublicationObjSpec *obj = makeNode(PublicationObjSpec);
+					obj->pubobjtype = PUBLICATIONOBJ_TABLE;
+					obj->pubtable = makeNode(PublicationTable);
+					obj->pubtable->relation = $6;
+					obj->pubtable->columns = $10;
+					n->pubname = $3;
+					n->pubobjects = list_make1(obj);
+					n->action = AP_SetColumns;
+					$$ = (Node *) n;
+				}
+			| ALTER PUBLICATION name ALTER TABLE relation_expr SET COLUMNS ALL
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					PublicationObjSpec *obj = makeNode(PublicationObjSpec);
+					obj->pubobjtype = PUBLICATIONOBJ_TABLE;
+					obj->pubtable = makeNode(PublicationTable);
+					obj->pubtable->relation = $6;
+					obj->pubtable->columns = NIL;
+					n->pubname = $3;
+					n->pubobjects = list_make1(obj);
+					n->action = AP_SetColumns;
+					$$ = (Node *) n;
+				}
 			| ALTER PUBLICATION name DROP pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
@@ -17462,6 +17502,16 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA)
 		{
+			/*
+			 * This can happen if a column list is specified in a continuation
+			 * for a schema entry; reject it.
+			 */
+			if (pubobj->pubtable)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("column specification not allowed for schemas"),
+						parser_errposition(pubobj->location));
+
 			/*
 			 * We can distinguish between the different type of schema
 			 * objects based on whether name and pubtable is set.
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index 953942692ce..e6da46d83e5 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -29,9 +29,11 @@
 #define TRUNCATE_CASCADE		(1<<0)
 #define TRUNCATE_RESTART_SEQS	(1<<1)
 
-static void logicalrep_write_attrs(StringInfo out, Relation rel);
+static void logicalrep_write_attrs(StringInfo out, Relation rel,
+								   Bitmapset *columns);
 static void logicalrep_write_tuple(StringInfo out, Relation rel,
-								   HeapTuple tuple, bool binary);
+								   HeapTuple tuple, bool binary,
+								   Bitmapset *columns);
 
 static void logicalrep_read_attrs(StringInfo in, LogicalRepRelation *rel);
 static void logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple);
@@ -398,7 +400,7 @@ logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn)
  */
 void
 logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
-						HeapTuple newtuple, bool binary)
+						HeapTuple newtuple, bool binary, Bitmapset *columns)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_INSERT);
 
@@ -410,7 +412,7 @@ logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
 	pq_sendint32(out, RelationGetRelid(rel));
 
 	pq_sendbyte(out, 'N');		/* new tuple follows */
-	logicalrep_write_tuple(out, rel, newtuple, binary);
+	logicalrep_write_tuple(out, rel, newtuple, binary, columns);
 }
 
 /*
@@ -442,7 +444,8 @@ logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup)
  */
 void
 logicalrep_write_update(StringInfo out, TransactionId xid, Relation rel,
-						HeapTuple oldtuple, HeapTuple newtuple, bool binary)
+						HeapTuple oldtuple, HeapTuple newtuple, bool binary,
+						Bitmapset *columns)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_UPDATE);
 
@@ -463,11 +466,11 @@ logicalrep_write_update(StringInfo out, TransactionId xid, Relation rel,
 			pq_sendbyte(out, 'O');	/* old tuple follows */
 		else
 			pq_sendbyte(out, 'K');	/* old key follows */
-		logicalrep_write_tuple(out, rel, oldtuple, binary);
+		logicalrep_write_tuple(out, rel, oldtuple, binary, columns);
 	}
 
 	pq_sendbyte(out, 'N');		/* new tuple follows */
-	logicalrep_write_tuple(out, rel, newtuple, binary);
+	logicalrep_write_tuple(out, rel, newtuple, binary, columns);
 }
 
 /*
@@ -536,7 +539,7 @@ logicalrep_write_delete(StringInfo out, TransactionId xid, Relation rel,
 	else
 		pq_sendbyte(out, 'K');	/* old key follows */
 
-	logicalrep_write_tuple(out, rel, oldtuple, binary);
+	logicalrep_write_tuple(out, rel, oldtuple, binary, NULL);
 }
 
 /*
@@ -651,7 +654,8 @@ logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
  * Write relation description to the output stream.
  */
 void
-logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
+logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel,
+					 Bitmapset *columns)
 {
 	char	   *relname;
 
@@ -673,7 +677,7 @@ logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
 	pq_sendbyte(out, rel->rd_rel->relreplident);
 
 	/* send the attribute info */
-	logicalrep_write_attrs(out, rel);
+	logicalrep_write_attrs(out, rel, columns);
 }
 
 /*
@@ -749,7 +753,8 @@ logicalrep_read_typ(StringInfo in, LogicalRepTyp *ltyp)
  * Write a tuple to the outputstream, in the most efficient format possible.
  */
 static void
-logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple, bool binary)
+logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple,
+					   bool binary, Bitmapset *columns)
 {
 	TupleDesc	desc;
 	Datum		values[MaxTupleAttributeNumber];
@@ -761,7 +766,13 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple, bool binar
 
 	for (i = 0; i < desc->natts; i++)
 	{
-		if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
+		Form_pg_attribute att = TupleDescAttr(desc, i);
+
+		if (att->attisdropped || att->attgenerated)
+			continue;
+
+		/* Don't count attributes that are not to be sent. */
+		if (columns != NULL && !bms_is_member(att->attnum, columns))
 			continue;
 		nliveatts++;
 	}
@@ -783,6 +794,10 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple, bool binar
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
+		/* Ignore attributes that are not to be sent. */
+		if (columns != NULL && !bms_is_member(att->attnum, columns))
+			continue;
+
 		if (isnull[i])
 		{
 			pq_sendbyte(out, LOGICALREP_COLUMN_NULL);
@@ -904,7 +919,7 @@ logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple)
  * Write relation attribute metadata to the stream.
  */
 static void
-logicalrep_write_attrs(StringInfo out, Relation rel)
+logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 {
 	TupleDesc	desc;
 	int			i;
@@ -914,20 +929,24 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 
 	desc = RelationGetDescr(rel);
 
+	/* fetch bitmap of REPLICATION IDENTITY attributes */
+	replidentfull = (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
+	if (!replidentfull)
+		idattrs = RelationGetIdentityKeyBitmap(rel);
+
 	/* send number of live attributes */
 	for (i = 0; i < desc->natts; i++)
 	{
-		if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
+		Form_pg_attribute att = TupleDescAttr(desc, i);
+
+		if (att->attisdropped || att->attgenerated)
+			continue;
+		if (columns != NULL && !bms_is_member(att->attnum, columns))
 			continue;
 		nliveatts++;
 	}
 	pq_sendint16(out, nliveatts);
 
-	/* fetch bitmap of REPLICATION IDENTITY attributes */
-	replidentfull = (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
-	if (!replidentfull)
-		idattrs = RelationGetIdentityKeyBitmap(rel);
-
 	/* send the attributes */
 	for (i = 0; i < desc->natts; i++)
 	{
@@ -936,7 +955,8 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 
 		if (att->attisdropped || att->attgenerated)
 			continue;
-
+		if (columns != NULL && !bms_is_member(att->attnum, columns))
+			continue;
 		/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
 		if (replidentfull ||
 			bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index e596b69d466..a7befd712a0 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -112,6 +112,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "utils/acl.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -699,17 +700,20 @@ fetch_remote_table_info(char *nspname, char *relname,
 	WalRcvExecResult *res;
 	StringInfoData cmd;
 	TupleTableSlot *slot;
-	Oid			tableRow[] = {OIDOID, CHAROID, CHAROID};
-	Oid			attrRow[] = {TEXTOID, OIDOID, BOOLOID};
+	Oid			tableRow[] = {OIDOID, CHAROID, CHAROID, BOOLOID};
+	Oid			attrRow[] = {INT2OID, TEXTOID, OIDOID, BOOLOID};
 	bool		isnull;
 	int			natt;
+	ListCell   *lc;
+	bool		am_partition;
+	Bitmapset  *included_cols = NULL;
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
 	/* First fetch Oid and replica identity. */
 	initStringInfo(&cmd);
-	appendStringInfo(&cmd, "SELECT c.oid, c.relreplident, c.relkind"
+	appendStringInfo(&cmd, "SELECT c.oid, c.relreplident, c.relkind, c.relispartition"
 					 "  FROM pg_catalog.pg_class c"
 					 "  INNER JOIN pg_catalog.pg_namespace n"
 					 "        ON (c.relnamespace = n.oid)"
@@ -739,14 +743,19 @@ fetch_remote_table_info(char *nspname, char *relname,
 	Assert(!isnull);
 	lrel->relkind = DatumGetChar(slot_getattr(slot, 3, &isnull));
 	Assert(!isnull);
+	am_partition = DatumGetBool(slot_getattr(slot, 4, &isnull));
+	Assert(!isnull);
 
 	ExecDropSingleTupleTableSlot(slot);
 	walrcv_clear_result(res);
 
-	/* Now fetch columns. */
+	/*
+	 * Now fetch column names and types.
+	 */
 	resetStringInfo(&cmd);
 	appendStringInfo(&cmd,
-					 "SELECT a.attname,"
+					 "SELECT a.attnum,"
+					 "       a.attname,"
 					 "       a.atttypid,"
 					 "       a.attnum = ANY(i.indkey)"
 					 "  FROM pg_catalog.pg_attribute a"
@@ -774,16 +783,91 @@ fetch_remote_table_info(char *nspname, char *relname,
 	lrel->atttyps = palloc0(MaxTupleAttributeNumber * sizeof(Oid));
 	lrel->attkeys = NULL;
 
+	/*
+	 * In server versions 15 and higher, obtain the publication column list,
+	 * if any.
+	 */
+	if (walrcv_server_version(LogRepWorkerWalRcvConn) >= 150000)
+	{
+		WalRcvExecResult *pubres;
+		TupleTableSlot *slot;
+		Oid			attrsRow[] = {INT2OID};
+		StringInfoData publications;
+		bool		first = true;
+
+		initStringInfo(&publications);
+		foreach(lc, MySubscription->publications)
+		{
+			if (!first)
+				appendStringInfo(&publications, ", ");
+			appendStringInfoString(&publications, quote_literal_cstr(strVal(lfirst(lc))));
+			first = false;
+		}
+
+		resetStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "  SELECT pg_catalog.unnest(prattrs)\n"
+						 "    FROM pg_catalog.pg_publication p JOIN\n"
+						 "         pg_catalog.pg_publication_rel pr ON (p.oid = pr.prpubid)\n"
+						 "   WHERE p.pubname IN (%s) AND\n",
+						 publications.data);
+		if (!am_partition)
+			appendStringInfo(&cmd, "prrelid = %u", lrel->remoteid);
+		else
+			appendStringInfo(&cmd,
+							 "prrelid IN (SELECT relid\n"
+							 "    FROM pg_catalog.pg_partition_tree(pg_catalog.pg_partition_root(%u)))",
+							 lrel->remoteid);
+
+		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+							 lengthof(attrsRow), attrsRow);
+
+		if (pubres->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not fetch attribute info for table \"%s.%s\" from publisher: %s",
+							nspname, relname, pubres->err)));
+
+		slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+		while (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+		{
+			AttrNumber	attnum;
+
+			attnum = DatumGetInt16(slot_getattr(slot, 1, &isnull));
+			if (isnull)
+				continue;
+			included_cols = bms_add_member(included_cols, attnum);
+		}
+		ExecDropSingleTupleTableSlot(slot);
+		pfree(publications.data);
+		walrcv_clear_result(pubres);
+	}
+
+	/*
+	 * Store the columns as a list of names.  Ignore those that are not
+	 * present in the column list, if there is one.
+	 */
 	natt = 0;
 	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
 	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
 	{
-		lrel->attnames[natt] =
-			TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		char	   *rel_colname;
+		AttrNumber	attnum;
+
+		attnum = DatumGetInt16(slot_getattr(slot, 1, &isnull));
 		Assert(!isnull);
-		lrel->atttyps[natt] = DatumGetObjectId(slot_getattr(slot, 2, &isnull));
+
+		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+			continue;
+
+		rel_colname = TextDatumGetCString(slot_getattr(slot, 2, &isnull));
 		Assert(!isnull);
-		if (DatumGetBool(slot_getattr(slot, 3, &isnull)))
+
+		lrel->attnames[natt] = rel_colname;
+		lrel->atttyps[natt] = DatumGetObjectId(slot_getattr(slot, 3, &isnull));
+		Assert(!isnull);
+
+		if (DatumGetBool(slot_getattr(slot, 4, &isnull)))
 			lrel->attkeys = bms_add_member(lrel->attkeys, natt);
 
 		/* Should never happen. */
@@ -793,12 +877,12 @@ fetch_remote_table_info(char *nspname, char *relname,
 
 		ExecClearTuple(slot);
 	}
-	ExecDropSingleTupleTableSlot(slot);
-
-	lrel->natts = natt;
 
+	ExecDropSingleTupleTableSlot(slot);
 	walrcv_clear_result(res);
 	pfree(cmd.data);
+
+	lrel->natts = natt;
 }
 
 /*
@@ -831,8 +915,17 @@ copy_table(Relation rel)
 	/* Start copy on the publisher. */
 	initStringInfo(&cmd);
 	if (lrel.relkind == RELKIND_RELATION)
-		appendStringInfo(&cmd, "COPY %s TO STDOUT",
+	{
+		appendStringInfo(&cmd, "COPY %s (",
 						 quote_qualified_identifier(lrel.nspname, lrel.relname));
+		for (int i = 0; i < lrel.natts; i++)
+		{
+			appendStringInfoString(&cmd, quote_identifier(lrel.attnames[i]));
+			if (i < lrel.natts - 1)
+				appendStringInfoString(&cmd, ", ");
+		}
+		appendStringInfo(&cmd, ") TO STDOUT");
+	}
 	else
 	{
 		/*
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 6df705f90ff..c7b28d70d23 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -15,16 +15,19 @@
 #include "access/tupconvert.h"
 #include "catalog/partition.h"
 #include "catalog/pg_publication.h"
+#include "catalog/pg_publication_rel_d.h"
 #include "commands/defrem.h"
 #include "fmgr.h"
 #include "replication/logical.h"
 #include "replication/logicalproto.h"
 #include "replication/origin.h"
 #include "replication/pgoutput.h"
+#include "utils/builtins.h"
 #include "utils/int8.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
@@ -81,7 +84,8 @@ static List *LoadPublications(List *pubnames);
 static void publication_invalidation_cb(Datum arg, int cacheid,
 										uint32 hashvalue);
 static void send_relation_and_attrs(Relation relation, TransactionId xid,
-									LogicalDecodingContext *ctx);
+									LogicalDecodingContext *ctx,
+									Bitmapset *columns);
 static void send_repl_origin(LogicalDecodingContext *ctx,
 							 RepOriginId origin_id, XLogRecPtr origin_lsn,
 							 bool send_origin);
@@ -132,6 +136,13 @@ typedef struct RelationSyncEntry
 	 * having identical TupleDesc.
 	 */
 	TupleConversionMap *map;
+
+	/*
+	 * Set of columns included in the publication, or NULL if all columns are
+	 * included implicitly.  Note that the attnums in this list are not
+	 * shifted by FirstLowInvalidHeapAttributeNumber.
+	 */
+	Bitmapset  *columns;
 } RelationSyncEntry;
 
 /* Map used to remember which relation schemas we sent. */
@@ -572,11 +583,11 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 		}
 
 		MemoryContextSwitchTo(oldctx);
-		send_relation_and_attrs(ancestor, xid, ctx);
+		send_relation_and_attrs(ancestor, xid, ctx, relentry->columns);
 		RelationClose(ancestor);
 	}
 
-	send_relation_and_attrs(relation, xid, ctx);
+	send_relation_and_attrs(relation, xid, ctx, relentry->columns);
 
 	if (in_streaming)
 		set_schema_sent_in_streamed_txn(relentry, topxid);
@@ -589,7 +600,8 @@ maybe_send_schema(LogicalDecodingContext *ctx,
  */
 static void
 send_relation_and_attrs(Relation relation, TransactionId xid,
-						LogicalDecodingContext *ctx)
+						LogicalDecodingContext *ctx,
+						Bitmapset *columns)
 {
 	TupleDesc	desc = RelationGetDescr(relation);
 	int			i;
@@ -612,13 +624,17 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		if (att->atttypid < FirstGenbkiObjectId)
 			continue;
 
+		/* Skip this attribute if it's not present in the column list */
+		if (columns != NULL && !bms_is_member(att->attnum, columns))
+			continue;
+
 		OutputPluginPrepareWrite(ctx, false);
 		logicalrep_write_typ(ctx->out, xid, att->atttypid);
 		OutputPluginWrite(ctx, false);
 	}
 
 	OutputPluginPrepareWrite(ctx, false);
-	logicalrep_write_rel(ctx->out, xid, relation);
+	logicalrep_write_rel(ctx->out, xid, relation, columns);
 	OutputPluginWrite(ctx, false);
 }
 
@@ -695,7 +711,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 				OutputPluginPrepareWrite(ctx, true);
 				logicalrep_write_insert(ctx->out, xid, relation, tuple,
-										data->binary);
+										data->binary, relentry->columns);
 				OutputPluginWrite(ctx, true);
 				break;
 			}
@@ -724,7 +740,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 				OutputPluginPrepareWrite(ctx, true);
 				logicalrep_write_update(ctx->out, xid, relation, oldtuple,
-										newtuple, data->binary);
+										newtuple, data->binary, relentry->columns);
 				OutputPluginWrite(ctx, true);
 				break;
 			}
@@ -1124,6 +1140,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 {
 	RelationSyncEntry *entry;
 	bool		found;
+	Oid			ancestor_id;
 	MemoryContext oldctx;
 
 	Assert(RelationSyncCache != NULL);
@@ -1143,6 +1160,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 		entry->pubactions.pubinsert = entry->pubactions.pubupdate =
 			entry->pubactions.pubdelete = entry->pubactions.pubtruncate = false;
 		entry->publish_as_relid = InvalidOid;
+		entry->columns = NULL;
 		entry->map = NULL;		/* will be set by maybe_send_schema() if
 								 * needed */
 	}
@@ -1187,6 +1205,8 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 		entry->schema_sent = false;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
+		bms_free(entry->columns);
+		entry->columns = NULL;
 		entry->pubactions.pubinsert = false;
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
@@ -1205,13 +1225,16 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 
 		/*
 		 * Build publication cache. We can't use one provided by relcache as
-		 * relcache considers all publications given relation is in, but here
-		 * we only need to consider ones that the subscriber requested.
+		 * relcache considers all publications that the given relation is in,
+		 * but here we only need to consider ones that the subscriber
+		 * requested.
 		 */
 		foreach(lc, data->publications)
 		{
 			Publication *pub = lfirst(lc);
 			bool		publish = false;
+			bool		ancestor_published = false;
+			bool		all_columns = false;
 
 			if (pub->alltables)
 			{
@@ -1222,8 +1245,6 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 
 			if (!publish)
 			{
-				bool		ancestor_published = false;
-
 				/*
 				 * For a partition, check if any of the ancestors are
 				 * published.  If so, note down the topmost ancestor that is
@@ -1249,6 +1270,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 							list_member_oid(aschemaPubids, pub->oid))
 						{
 							ancestor_published = true;
+							ancestor_id = ancestor;
 							if (pub->pubviaroot)
 								publish_as_relid = ancestor;
 						}
@@ -1264,9 +1286,13 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 			}
 
 			/*
+			 * If the relation is to be published, determine actions to
+			 * publish, and list of columns, if appropriate.
+			 *
 			 * Don't publish changes for partitioned tables, because
-			 * publishing those of its partitions suffices, unless partition
-			 * changes won't be published due to pubviaroot being set.
+			 * publishing those of its partitions suffices.  (However, ignore
+			 * this if partition changes are not to published due to
+			 * pubviaroot being set.)
 			 */
 			if (publish &&
 				(relkind != RELKIND_PARTITIONED_TABLE || pub->pubviaroot))
@@ -1275,10 +1301,74 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 				entry->pubactions.pubupdate |= pub->pubactions.pubupdate;
 				entry->pubactions.pubdelete |= pub->pubactions.pubdelete;
 				entry->pubactions.pubtruncate |= pub->pubactions.pubtruncate;
+
+				/*
+				 * Obtain columns published by this publication, and add them
+				 * to the list for this rel.  Note that if at least one
+				 * publication has a empty column list, that means to publish
+				 * everything; so if we saw a publication that includes all
+				 * columns, skip this.
+				 */
+				if (!all_columns)
+				{
+					HeapTuple	pub_rel_tuple;
+					Oid			relid;
+
+					relid = ancestor_published ? ancestor_id : publish_as_relid;
+
+					pub_rel_tuple = SearchSysCache2(PUBLICATIONRELMAP,
+													ObjectIdGetDatum(relid),
+													ObjectIdGetDatum(pub->oid));
+
+					if (HeapTupleIsValid(pub_rel_tuple))
+					{
+						Datum		pub_rel_cols;
+						bool		isnull;
+
+						pub_rel_cols = SysCacheGetAttr(PUBLICATIONRELMAP,
+													   pub_rel_tuple,
+													   Anum_pg_publication_rel_prattrs,
+													   &isnull);
+						if (isnull)
+						{
+							/*
+							 * If we see a publication with no columns, reset the
+							 * list and ignore further ones.
+							 */
+							all_columns = true;
+							bms_free(entry->columns);
+							entry->columns = NULL;
+						}
+						else if (!isnull)
+						{
+							ArrayType  *arr;
+							int			nelems;
+							int16	   *elems;
+
+							arr = DatumGetArrayTypeP(pub_rel_cols);
+							nelems = ARR_DIMS(arr)[0];
+							elems = (int16 *) ARR_DATA_PTR(arr);
+
+							/* XXX is there a danger of memory leak here? beware */
+							oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+							for (int i = 0; i < nelems; i++)
+								entry->columns = bms_add_member(entry->columns,
+																elems[i]);
+							MemoryContextSwitchTo(oldctx);
+						}
+
+						ReleaseSysCache(pub_rel_tuple);
+					}
+				}
 			}
 
+			/*
+			 * If we've seen all action bits, and we know that all columns are
+			 * published, there's no reason to look at further publications.
+			 */
 			if (entry->pubactions.pubinsert && entry->pubactions.pubupdate &&
-				entry->pubactions.pubdelete && entry->pubactions.pubtruncate)
+				entry->pubactions.pubdelete && entry->pubactions.pubtruncate &&
+				all_columns)
 				break;
 		}
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4485ea83b1e..2c347e80ee0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4074,6 +4074,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_oid;
 	int			i_prpubid;
 	int			i_prrelid;
+	int			i_prattrs;
 	int			i,
 				j,
 				ntups;
@@ -4085,8 +4086,13 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	appendPQExpBufferStr(query,
-						 "SELECT tableoid, oid, prpubid, prrelid "
-						 "FROM pg_catalog.pg_publication_rel");
+						 "SELECT tableoid, oid, prpubid, prrelid");
+	if (fout->remoteVersion >= 150000)
+		appendPQExpBufferStr(query, ", prattrs");
+	else
+		appendPQExpBufferStr(query, ", NULL as prattrs");
+	appendPQExpBufferStr(query,
+						 " FROM pg_catalog.pg_publication_rel");
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
 	ntups = PQntuples(res);
@@ -4095,6 +4101,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_oid = PQfnumber(res, "oid");
 	i_prpubid = PQfnumber(res, "prpubid");
 	i_prrelid = PQfnumber(res, "prrelid");
+	i_prattrs = PQfnumber(res, "prattrs");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4136,6 +4143,28 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		pubrinfo[j].publication = pubinfo;
 		pubrinfo[j].pubtable = tbinfo;
 
+		if (!PQgetisnull(res, i, i_prattrs))
+		{
+			char	  **attnames;
+			int			nattnames;
+			PQExpBuffer attribs;
+
+			if (!parsePGArray(PQgetvalue(res, i, i_prattrs),
+							  &attnames, &nattnames))
+				fatal("could not parse %s array", "prattrs");
+			attribs = createPQExpBuffer();
+			for (int k = 0; k < nattnames; k++)
+			{
+				if (k > 0)
+					appendPQExpBufferStr(attribs, ", ");
+
+				appendPQExpBufferStr(attribs, fmtId(attnames[k]));
+			}
+			pubrinfo[i].pubrattrs = attribs->data;
+		}
+		else
+			pubrinfo[j].pubrattrs = NULL;
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
@@ -4210,10 +4239,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY ",
 					  fmtId(pubinfo->dobj.name));
-	appendPQExpBuffer(query, " %s;\n",
-					  fmtQualifiedDumpable(tbinfo));
+	appendPQExpBufferStr(query, fmtQualifiedDumpable(tbinfo));
+	if (pubrinfo->pubrattrs)
+		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	appendPQExpBufferStr(query, ";\n");
 
 	/*
 	 * There is no point in creating a drop query as the drop is done by table
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9965ac2518b..54df6996428 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -631,6 +631,7 @@ typedef struct _PublicationRelInfo
 	DumpableObject dobj;
 	PublicationInfo *publication;
 	TableInfo  *pubtable;
+	char	   *pubrattrs;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 654ef2d7c30..1d1474a8c29 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5856,7 +5856,7 @@ listPublications(const char *pattern)
  */
 static bool
 addFooterToPublicationDesc(PQExpBuffer buf, char *footermsg,
-						   bool singlecol, printTableContent *cont)
+						   bool as_schema, printTableContent *cont)
 {
 	PGresult   *res;
 	int			count = 0;
@@ -5873,10 +5873,14 @@ addFooterToPublicationDesc(PQExpBuffer buf, char *footermsg,
 
 	for (i = 0; i < count; i++)
 	{
-		if (!singlecol)
+		if (!as_schema)			/* as table */
+		{
 			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
 							  PQgetvalue(res, i, 1));
-		else
+			if (!PQgetisnull(res, i, 2))
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 2));
+		}
+		else					/* as schema */
 			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
 
 		printTableAddFooter(cont, buf->data);
@@ -6004,8 +6008,20 @@ describePublications(const char *pattern)
 		{
 			/* Get the tables for the specified publication */
 			printfPQExpBuffer(&buf,
-							  "SELECT n.nspname, c.relname\n"
-							  "FROM pg_catalog.pg_class c,\n"
+							  "SELECT n.nspname, c.relname, \n");
+			if (pset.sversion >= 150000)
+				appendPQExpBufferStr(&buf,
+									 "       CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+									 "       pg_catalog.array_to_string"
+									 "(ARRAY(SELECT attname\n"
+									 "         FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+									 "              pg_catalog.pg_attribute\n"
+									 "        WHERE attrelid = c.oid AND attnum = prattrs[s]), ', ')\n"
+									 "       ELSE NULL END AS columns");
+			else
+				appendPQExpBufferStr(&buf, "NULL as columns");
+			appendPQExpBuffer(&buf,
+							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 841b9b6c253..70f73d01dc0 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -86,6 +86,7 @@ typedef struct Publication
 typedef struct PublicationRelInfo
 {
 	Relation	relation;
+	List	   *columns;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
@@ -109,8 +110,11 @@ typedef enum PublicationPartOpt
 } PublicationPartOpt;
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetRelationColumnPartialPublications(Oid relid);
+extern List *GetRelationColumnListInPublication(Oid relid, Oid pubid);
 extern List *GetAllTablesPublications(void);
 extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern void GetActionsInPublication(Oid pubid, PublicationActions *actions);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -127,6 +131,8 @@ extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *tar
 											  bool if_not_exists);
 extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 											bool if_not_exists);
+extern void publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
+										  Relation targetrel, List *columns);
 
 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 117a1d67e56..291ab73fee7 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,9 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	int2vector	prattrs;
+#endif
 } FormData_pg_publication_rel;
 
 /* ----------------
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 34218b718c1..1becb5b78ea 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3651,6 +3651,7 @@ typedef struct PublicationTable
 {
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to be published */
+	List	   *columns;		/* List of columns in a publication table */
 } PublicationTable;
 
 /*
@@ -3687,7 +3688,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_SetColumns				/* change list of columns for a table */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h
index 22fffaca62d..fcbed4ed2d6 100644
--- a/src/include/replication/logicalproto.h
+++ b/src/include/replication/logicalproto.h
@@ -207,11 +207,11 @@ extern void logicalrep_write_origin(StringInfo out, const char *origin,
 extern char *logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn);
 extern void logicalrep_write_insert(StringInfo out, TransactionId xid,
 									Relation rel, HeapTuple newtuple,
-									bool binary);
+									bool binary, Bitmapset *columns);
 extern LogicalRepRelId logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup);
 extern void logicalrep_write_update(StringInfo out, TransactionId xid,
 									Relation rel, HeapTuple oldtuple,
-									HeapTuple newtuple, bool binary);
+									HeapTuple newtuple, bool binary, Bitmapset *columns);
 extern LogicalRepRelId logicalrep_read_update(StringInfo in,
 											  bool *has_oldtuple, LogicalRepTupleData *oldtup,
 											  LogicalRepTupleData *newtup);
@@ -228,7 +228,7 @@ extern List *logicalrep_read_truncate(StringInfo in,
 extern void logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
 									 bool transactional, const char *prefix, Size sz, const char *message);
 extern void logicalrep_write_rel(StringInfo out, TransactionId xid,
-								 Relation rel);
+								 Relation rel, Bitmapset *columns);
 extern LogicalRepRelation *logicalrep_read_rel(StringInfo in);
 extern void logicalrep_write_typ(StringInfo out, TransactionId xid,
 								 Oid typoid);
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index b97f98cda72..861eaf6f695 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,8 +165,54 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
-DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
+	d int generated always as (a + length(b)) stored);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);  -- error
+ERROR:  column "x" of relation "testpub_tbl5" does not exist
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);  -- error
+ERROR:  invalid column list for publishing relation "testpub_tbl5"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);  -- error
+ERROR:  cannot reference generated column "d" in publication column list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);  -- ok
+ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
+ERROR:  cannot drop column "c" because it is part of publication "testpub_fortable"
+HINT:  Specify CASCADE or use ALTER PUBLICATION to remove the column from the publication.
+/* not all replica identities are good enough */
+CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
+ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key; -- nope
+ERROR:  index "testpub_tbl5_b_key" cannot be used because publication "testpub_fortable" does not include all indexed columns
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key; -- ok, but ...
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);	-- no dice
+ERROR:  invalid column list for publishing relation "testpub_tbl5"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+/* But if upd/del are not published, it works OK */
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
+RESET client_min_messages;
+ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
+\dRp+ testpub_table_ins
+                               Publication testpub_table_ins
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | t         | f
+Tables:
+    "public.testpub_tbl5" (a)
+
+CREATE TABLE testpub_tbl6 (a int, b text, c text);
+ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);  -- error
+ERROR:  invalid column list for publishing relation "testpub_tbl6"
+DETAIL:  Cannot specify a column list on relations with REPLICA IDENTITY FULL.
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
+ALTER PUBLICATION testpub_fortable
+  ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);	-- error
+ERROR:  cannot change column set for relation "testpub_tbl6"
+DETAIL:  Cannot specify column list on relations with REPLICA IDENTITY FULL.
+DROP TABLE testpub_tbl2, testpub_tbl5, testpub_tbl6;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_table_ins;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
@@ -684,6 +730,15 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_tes
 Tables from schemas:
     "pub_test1"
 
+-- Verify that it fails to add a schema with a column specification
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo (a, b);
+ERROR:  syntax error at or near "("
+LINE 1: ...TION testpub1_forschema ADD ALL TABLES IN SCHEMA foo (a, b);
+                                                                ^
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo, bar (a, b);
+ERROR:  column specification not allowed for schemas
+LINE 1: ... testpub1_forschema ADD ALL TABLES IN SCHEMA foo, bar (a, b)...
+                                                             ^
 -- cleanup pub_test1 schema for invalidation tests
 ALTER PUBLICATION testpub2_forschema DROP ALL TABLES IN SCHEMA pub_test1;
 DROP PUBLICATION testpub3_forschema, testpub4_forschema, testpub5_forschema, testpub6_forschema, testpub_fortable;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 86c019bddb4..d0e9f2b11ba 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,8 +89,39 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
-DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
+	d int generated always as (a + length(b)) stored);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);  -- error
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);  -- error
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);  -- error
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);  -- ok
+ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
+
+/* not all replica identities are good enough */
+CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
+ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key; -- nope
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key; -- ok, but ...
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);	-- no dice
+
+/* But if upd/del are not published, it works OK */
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
+RESET client_min_messages;
+ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
+\dRp+ testpub_table_ins
+
+CREATE TABLE testpub_tbl6 (a int, b text, c text);
+ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);  -- error
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
+ALTER PUBLICATION testpub_fortable
+  ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);	-- error
+
+DROP TABLE testpub_tbl2, testpub_tbl5, testpub_tbl6;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_table_ins;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
@@ -375,6 +406,10 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA non_existent_schem
 ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_test1;
 \dRp+ testpub1_forschema
 
+-- Verify that it fails to add a schema with a column specification
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo (a, b);
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo, bar (a, b);
+
 -- cleanup pub_test1 schema for invalidation tests
 ALTER PUBLICATION testpub2_forschema DROP ALL TABLES IN SCHEMA pub_test1;
 DROP PUBLICATION testpub3_forschema, testpub4_forschema, testpub5_forschema, testpub6_forschema, testpub_fortable;
diff --git a/src/test/subscription/t/028_column_list.pl b/src/test/subscription/t/028_column_list.pl
new file mode 100644
index 00000000000..5a4f022f268
--- /dev/null
+++ b/src/test/subscription/t/028_column_list.pl
@@ -0,0 +1,164 @@
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+# Test partial-column publication of tables
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 9;
+
+# setup
+
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->append_conf('postgresql.conf',
+	qq(max_logical_replication_workers = 6));
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab1 (a int PRIMARY KEY, \"B\" int, c int)");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab1 (a int PRIMARY KEY, \"B\" int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int PRIMARY KEY, b varchar, c int);
+	INSERT INTO tab2 VALUES (2, 'foo', 2);");
+# Test with weird column names
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab3 (\"a'\" int PRIMARY KEY, B varchar, \"c'\" int)");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE test_part (a int PRIMARY KEY, b text, c timestamptz) PARTITION BY LIST (a)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3)");
+# Test replication with multi-level partition
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (4,5,6) PARTITION BY LIST (a)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (4,5)");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE test_part (a int PRIMARY KEY, b text) PARTITION BY LIST (a)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab3 (\"a'\" int PRIMARY KEY, \"c'\" int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int PRIMARY KEY, b varchar)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (4,5,6) PARTITION BY LIST (a)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (4,5)");
+
+# Test create publication with a column list
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION pub1 FOR TABLE tab1(a, \"B\"), tab3(\"a'\",\"c'\"), test_part(a,b)");
+
+my $result = $node_publisher->safe_psql('postgres',
+	"select relname, prattrs from pg_publication_rel pb, pg_class pc where pb.prrelid = pc.oid;");
+is($result, qq(tab1|1 2
+tab3|1 3
+test_part|1 2), 'publication relation updated');
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
+);
+# Initial sync
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab1 VALUES (1,2,3)");
+
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab3 VALUES (1,2,3)");
+# Test for replication of partition data
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_part VALUES (1,'abc', '2021-07-04 12:00:00')");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_part VALUES (2,'bcd', '2021-07-03 11:12:13')");
+# Test for replication of multi-level partition data
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_part VALUES (4,'abc', '2021-07-04 12:00:00')");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_part VALUES (5,'bcd', '2021-07-03 11:12:13')");
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab1");
+is($result, qq(1|2|), 'insert on column tab1.c is not replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab3");
+is($result, qq(1|3), 'insert on column tab3.b is not replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM test_part");
+is($result, qq(1|abc\n2|bcd\n4|abc\n5|bcd), 'insert on all columns is replicated');
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab1 SET c = 5 where a = 1");
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab1");
+is($result, qq(1|2|), 'update on column tab1.c is not replicated');
+
+# Verify user-defined types
+$node_publisher->safe_psql('postgres',
+	qq{CREATE TYPE test_typ AS ENUM ('blue', 'red');
+	CREATE TABLE test_tab4 (a INT PRIMARY KEY, b test_typ, c int, d text);
+	ALTER PUBLICATION pub1 ADD TABLE test_tab4 (a, b, d);
+	});
+$node_subscriber->safe_psql('postgres',
+	qq{CREATE TYPE test_typ AS ENUM ('blue', 'red');
+	CREATE TABLE test_tab4 (a INT PRIMARY KEY, b test_typ, d text);
+	});
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_tab4 VALUES (1, 'red', 3, 'oh my');");
+
+# Test alter publication with a column list
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION pub1 ADD TABLE tab2(a, b)");
+
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION"
+);
+
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab2 VALUES (1,'abc',3)");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET c = 5 where a = 2");
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab2 WHERE a = 1");
+is($result, qq(1|abc), 'insert on column tab2.c is not replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab2 WHERE a = 2");
+is($result, qq(2|foo), 'update on column tab2.c is not replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM test_tab4");
+is($result, qq(1|red|oh my), 'insert on table with user-defined type');
+
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab5 (a int PRIMARY KEY, b int, c int, d int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab5 (a int PRIMARY KEY, b int, d int)");
+$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub2 FOR TABLE tab5 (a, b)");
+$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub3 FOR TABLE tab5 (a, d)");
+$node_subscriber->safe_psql('postgres',    "CREATE SUBSCRIPTION sub2 CONNECTION '$publisher_connstr' PUBLICATION pub2, pub3");
+$node_publisher->wait_for_catchup('sub2');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (1, 11, 111, 1111)");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (2, 22, 222, 2222)");
+$node_publisher->wait_for_catchup('sub2');
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab5;"),
+   qq(1|11|1111
+2|22|2222),
+   'overlapping publications with overlapping column lists');
-- 
2.34.1

From 777e2cd1b9dea015c32bf606c76f205561106928 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 15 Feb 2022 11:04:05 +0100
Subject: [PATCH 2/4] commit tweaks and reviews

---
 doc/src/sgml/ref/alter_publication.sgml |  3 +-
 src/backend/catalog/pg_publication.c    | 50 +++++++++++++++++--------
 src/backend/commands/publicationcmds.c  |  6 +++
 src/backend/replication/logical/proto.c | 12 +++++-
 4 files changed, 54 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index a85574214a5..6a0c7722c7d 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -65,7 +65,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    The <literal>ALTER TABLE ... SET COLUMNS</literal> variant allows changing
-   the set of columns that are included in the publication.
+   the set of columns that are included in the publication.  If a column list
+   is specified, it must include the replica identity columns.
   </para>
 
   <para>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index aa1655696f5..f8e0a728a8c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -56,14 +56,14 @@ static void publication_translate_columns(Relation targetrel, List *columns,
  * Check if relation can be in given publication and that the column
  * filter is sensible, and throws appropriate error if not.
  *
- * targetcols is the bitmapset of attribute numbers given in the column list,
- * or NULL if it was not specified.
+ * columns is the bitmapset of attribute numbers included in the column list,
+ * or NULL if no column list was specified (i.e. all columns are replicated)
  */
 static void
 check_publication_add_relation(Publication *pub, Relation targetrel,
 							   Bitmapset *columns)
 {
-	bool		replidentfull = (targetrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
+	bool	replidentfull = (targetrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
 
 	/* Must be a regular or partitioned table */
 	if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -96,17 +96,24 @@ check_publication_add_relation(Publication *pub, Relation targetrel,
 						RelationGetRelationName(targetrel)),
 				 errdetail("This operation is not supported for unlogged tables.")));
 
-	/* Make sure the column list checks out */
+	/*
+	 * If a column filter was specified, check it's sensible with respect to
+	 * replica identity - the list has to include all columns in the replica
+	 * identity.
+	 */
 	if (columns != NULL)
 	{
 		/*
-		 * Even if the user listed all columns in the column list, we cannot
-		 * allow a column list to be specified when REPLICA IDENTITY is FULL;
-		 * that would cause problems if a new column is added later, because
-		 * the new column would have to be included (because of being part of
-		 * the replica identity) but it's technically not allowed (because of
-		 * not being in the publication's column list yet).  So reject this
-		 * case altogether.
+		 *
+		 * If the relation uses REPLICA IDENTITY FULL, we can't allow any column
+		 * list even if it lists all columns of the relation - it'd cause issues
+		 * if a column is added later. The column would become part of a replica
+		 * identity, violating the rule that the column list includes the whole
+		 * replica identity. We could add the column to the column list too, of
+		 * course, but it seems rather useles - the column list would always
+		 * include all columns, i.e. as if there's no column filter.
+		 *
+		 * So just reject this case altogether.
 		 */
 		if (replidentfull)
 			ereport(ERROR,
@@ -126,6 +133,10 @@ check_publication_add_relation(Publication *pub, Relation targetrel,
  * No column can be excluded if REPLICA IDENTITY is FULL (since all the
  * columns need to be sent regardless); and in other cases, the columns in
  * the REPLICA IDENTITY cannot be left out.
+ *
+ * Then we need to cross-check the replica identity and column list. For
+ * UPDATE/DELETE we need to ensure the replica identity is a subset of the
+ * column filter. For INSERT, we don't need to check anything.
  */
 static void
 check_publication_columns(Publication *pub, Relation targetrel, Bitmapset *columns)
@@ -513,11 +524,15 @@ publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
 									  &natts, &attarray, &attset);
 
 		/*
-		 * Make sure the column list checks out.  XXX this should occur at
-		 * caller in publicationcmds.c, not here.
+		 * Make sure the column list checks out.
+		 *
+		 * XXX this should occur at caller in publicationcmds.c, not here.
+		 * XXX How come this does not check replica identity? Should this prevent
+		 * replica identity full, just like check_publication_add_relation?
 		 */
 		check_publication_columns(pub, targetrel, attset);
 		bms_free(attset);
+
 		/* XXX "pub" is leaked here */
 
 		prattrs = buildint2vector(attarray, natts);
@@ -542,7 +557,11 @@ publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
 	heap_freetuple(copytup);
 }
 
-/* qsort comparator for attnums */
+/*
+ * qsort comparator for attnums
+ *
+ * XXX We already have compare_int16, so maybe let's share that, somehow?
+ */
 static int
 compare_int16(const void *a, const void *b)
 {
@@ -556,7 +575,8 @@ compare_int16(const void *a, const void *b)
 /*
  * Translate a list of column names to an array of attribute numbers
  * and a Bitmapset with them; verify that each attribute is appropriate
- * to have in a publication column list.  Other checks are done later;
+ * to have in a publication column list (no system or generated attributes,
+ * no duplicates).  Additional checks with replica identity are done later;
  * see check_publication_columns.
  *
  * Note that the attribute numbers are *not* offset by
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 592f56dd61e..e6a8f53912d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -439,6 +439,12 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 							  &publish_via_partition_root_given,
 							  &publish_via_partition_root);
 
+	/*
+	 * FIXME check pubactions vs. replica identity, to ensure the replica
+	 * identity is included in the column filter. Only do this for update
+	 * and delete publications. See check_publication_columns.
+	 */
+
 	/* Everything ok, form a new tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index e6da46d83e5..28c10e4d1e4 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -794,7 +794,11 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple,
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
-		/* Ignore attributes that are not to be sent. */
+		/* Ignore attributes that are not to be sent.
+		 *
+		 * XXX Do we need the (columns != NULL) check? I don't think so, because
+		 * such bitmap has no members.
+		 */
 		if (columns != NULL && !bms_is_member(att->attnum, columns))
 			continue;
 
@@ -941,8 +945,11 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 
 		if (att->attisdropped || att->attgenerated)
 			continue;
+
+		/* XXX we should have a function/macro for this check */
 		if (columns != NULL && !bms_is_member(att->attnum, columns))
 			continue;
+
 		nliveatts++;
 	}
 	pq_sendint16(out, nliveatts);
@@ -955,8 +962,11 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 
 		if (att->attisdropped || att->attgenerated)
 			continue;
+
+		/* XXX we should have a function/macro for this check */
 		if (columns != NULL && !bms_is_member(att->attnum, columns))
 			continue;
+
 		/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
 		if (replidentfull ||
 			bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
-- 
2.34.1

From a268e9a2eb13ecd8bc5f1f9053f13605bc4a3629 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 15 Feb 2022 23:08:11 +0100
Subject: [PATCH 3/4] Peter Smith review

---
 doc/src/sgml/catalogs.sgml                  |  4 +-
 doc/src/sgml/ref/alter_publication.sgml     |  4 +-
 doc/src/sgml/ref/create_publication.sgml    | 20 +++++---
 src/backend/catalog/pg_publication.c        | 57 ++++++++-------------
 src/backend/commands/tablecmds.c            |  1 -
 src/backend/replication/logical/proto.c     | 10 ++--
 src/backend/replication/logical/tablesync.c | 11 ++--
 src/backend/replication/pgoutput/pgoutput.c | 16 +++---
 src/bin/pg_dump/pg_dump.c                   |  2 +-
 src/include/catalog/pg_publication.h        |  2 +
 10 files changed, 61 insertions(+), 66 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f20cb2b78ab..adb0819c120 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -4392,7 +4392,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para>
       <para>
        This is an array of <structfield>indnatts</structfield> values that
-       indicate which table columns this index indexes.  For example a value
+       indicate which table columns this index indexes.  For example, a value
        of <literal>1 3</literal> would mean that the first and the third table
        columns make up the index entries.  Key columns come before non-key
        (included) columns.  A zero in this array indicates that the
@@ -6335,7 +6335,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        This is an array of values that indicates which table columns are
        part of the publication.  For example a value of <literal>1 3</literal>
        would mean that the first and the third table columns are published.
-       A null value indicates that all attributes are published.
+       A null value indicates that all columns are published.
       </para></entry>
      </row>
     </tbody>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 6a0c7722c7d..5eae5cde499 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -25,13 +25,13 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replace
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET <replaceable class="parameter">publication_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replaceable class="parameter">publication_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
-ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ALTER TABLE <replaceable class="parameter">publication_object</replaceable> SET COLUMNS { ( <replaceable class="parameter">name</replaceable> [, ...] ) | ALL }
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ALTER TABLE <replaceable class="parameter">table_name</replaceable> SET COLUMNS { ( <replaceable class="parameter">name</replaceable> [, ...] ) | ALL }
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <replaceable>new_name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable>, [, ... ] ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index a59cd3f532a..da4d929e02a 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable>, [, ... ] ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -79,12 +79,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the listed columns are replicated;
-      any other columns are ignored for the purpose of replication through
-      this publication.  If no column list is specified, all columns of the
-      table are replicated through this publication, including any columns
-      added later.  If a column list is specified, it must include the replica
-      identity columns.
+      When a column list is specified, only the named columns are replicated.
+      If no column list is specified, all columns of the table are replicated
+      through this publication, including any columns added later.  If a column
+      list is specified, it must include the replica identity columns.
      </para>
 
      <para>
@@ -300,6 +298,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
    <structname>sales</structname>:
 <programlisting>
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
+</programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table <structname>users</structname>,
+   but replicates only columns <structname>user_id</structname> and
+   <structname>firstname</structname>:
+<programlisting>
+CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index f8e0a728a8c..ab5a345b3de 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -63,8 +63,6 @@ static void
 check_publication_add_relation(Publication *pub, Relation targetrel,
 							   Bitmapset *columns)
 {
-	bool	replidentfull = (targetrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
-
 	/* Must be a regular or partitioned table */
 	if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
 		RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
@@ -97,50 +95,39 @@ check_publication_add_relation(Publication *pub, Relation targetrel,
 				 errdetail("This operation is not supported for unlogged tables.")));
 
 	/*
-	 * If a column filter was specified, check it's sensible with respect to
-	 * replica identity - the list has to include all columns in the replica
-	 * identity.
+	 * Ensure the column filter is compatible with the replica identity and the
+	 * actions the publication is replicating.
 	 */
-	if (columns != NULL)
-	{
-		/*
-		 *
-		 * If the relation uses REPLICA IDENTITY FULL, we can't allow any column
-		 * list even if it lists all columns of the relation - it'd cause issues
-		 * if a column is added later. The column would become part of a replica
-		 * identity, violating the rule that the column list includes the whole
-		 * replica identity. We could add the column to the column list too, of
-		 * course, but it seems rather useles - the column list would always
-		 * include all columns, i.e. as if there's no column filter.
-		 *
-		 * So just reject this case altogether.
-		 */
-		if (replidentfull)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("invalid column list for publishing relation \"%s\"",
-						   RelationGetRelationName(targetrel)),
-					errdetail("Cannot specify a column list on relations with REPLICA IDENTITY FULL."));
-
-		check_publication_columns(pub, targetrel, columns);
-	}
+	check_publication_columns(pub, targetrel, columns);
 }
 
 /*
  * Enforce that the column list can only leave out columns that aren't
  * forced to be sent.
  *
- * No column can be excluded if REPLICA IDENTITY is FULL (since all the
- * columns need to be sent regardless); and in other cases, the columns in
- * the REPLICA IDENTITY cannot be left out.
+ * If the relation uses REPLICA IDENTITY FULL, we can't allow any column
+ * list even if it lists all columns of the relation - it'd cause issues
+ * if a column is added later. The column would become part of a replica
+ * identity, violating the rule that the column list includes the whole
+ * replica identity. We could add the column to the column list too, of
+ * course, but it seems rather useles - the column list would always
+ * include all columns, i.e. as if there's no column filter.
  *
- * Then we need to cross-check the replica identity and column list. For
- * UPDATE/DELETE we need to ensure the replica identity is a subset of the
- * column filter. For INSERT, we don't need to check anything.
+ * In other cases, the columns in the REPLICA IDENTITY cannot be left out,
+ * except when the publication replicates only inserts. So we check that
+ * for UPDATE/DELETE the replica identity is a subset of the column filter.
  */
 static void
 check_publication_columns(Publication *pub, Relation targetrel, Bitmapset *columns)
 {
+	/*
+	 * If there is no column list, we treat it as if the list contains all columns. In
+	 * which case there's nothing to check so we're done.
+	 */
+	if (!columns)
+		return;
+
+	/* With REPLICA IDENTITY FULL no column filter is allowed. */
 	if (targetrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 		ereport(ERROR,
 				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
@@ -497,7 +484,7 @@ publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
 	Datum		values[Natts_pg_publication_rel];
 
 	memset(values, 0, sizeof(values));
-	memset(nulls, 0, sizeof(nulls));
+	memset(nulls, false, sizeof(nulls));
 	memset(replaces, false, sizeof(replaces));
 
 	replaces[Anum_pg_publication_rel_prattrs - 1] = true;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 28de9329800..2bfb165e45d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -15872,7 +15872,6 @@ relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid,
 			CatalogTupleUpdate(pg_index, &pg_index_tuple->t_self, pg_index_tuple);
 			InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0,
 										 InvalidOid, is_internal);
-
 			/*
 			 * Invalidate the relcache for the table, so that after we commit
 			 * all sessions will refresh the table's replica identity index
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index 28c10e4d1e4..ec5a787cc3a 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -933,11 +933,6 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 
 	desc = RelationGetDescr(rel);
 
-	/* fetch bitmap of REPLICATION IDENTITY attributes */
-	replidentfull = (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
-	if (!replidentfull)
-		idattrs = RelationGetIdentityKeyBitmap(rel);
-
 	/* send number of live attributes */
 	for (i = 0; i < desc->natts; i++)
 	{
@@ -954,6 +949,11 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 	}
 	pq_sendint16(out, nliveatts);
 
+	/* fetch bitmap of REPLICATION IDENTITY attributes */
+	replidentfull = (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
+	if (!replidentfull)
+		idattrs = RelationGetIdentityKeyBitmap(rel);
+
 	/* send the attributes */
 	for (i = 0; i < desc->natts; i++)
 	{
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index a7befd712a0..d2dbf5f7b83 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -877,12 +877,12 @@ fetch_remote_table_info(char *nspname, char *relname,
 
 		ExecClearTuple(slot);
 	}
-
 	ExecDropSingleTupleTableSlot(slot);
-	walrcv_clear_result(res);
-	pfree(cmd.data);
 
 	lrel->natts = natt;
+
+	walrcv_clear_result(res);
+	pfree(cmd.data);
 }
 
 /*
@@ -920,9 +920,10 @@ copy_table(Relation rel)
 						 quote_qualified_identifier(lrel.nspname, lrel.relname));
 		for (int i = 0; i < lrel.natts; i++)
 		{
-			appendStringInfoString(&cmd, quote_identifier(lrel.attnames[i]));
-			if (i < lrel.natts - 1)
+			if (i > 0)
 				appendStringInfoString(&cmd, ", ");
+
+			appendStringInfoString(&cmd, quote_identifier(lrel.attnames[i]));
 		}
 		appendStringInfo(&cmd, ") TO STDOUT");
 	}
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index c7b28d70d23..0068ae25d3e 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -138,8 +138,8 @@ typedef struct RelationSyncEntry
 	TupleConversionMap *map;
 
 	/*
-	 * Set of columns included in the publication, or NULL if all columns are
-	 * included implicitly.  Note that the attnums in this list are not
+	 * Columns included in the publication, or NULL if all columns are
+	 * included implicitly.  Note that the attnums in this bitmap are not
 	 * shifted by FirstLowInvalidHeapAttributeNumber.
 	 */
 	Bitmapset  *columns;
@@ -1290,9 +1290,8 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 			 * publish, and list of columns, if appropriate.
 			 *
 			 * Don't publish changes for partitioned tables, because
-			 * publishing those of its partitions suffices.  (However, ignore
-			 * this if partition changes are not to published due to
-			 * pubviaroot being set.)
+			 * publishing those of its partitions suffices, unless partition
+			 * changes won't be published due to pubviaroot being set.
 			 */
 			if (publish &&
 				(relkind != RELKIND_PARTITIONED_TABLE || pub->pubviaroot))
@@ -1305,7 +1304,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 				/*
 				 * Obtain columns published by this publication, and add them
 				 * to the list for this rel.  Note that if at least one
-				 * publication has a empty column list, that means to publish
+				 * publication has an empty column list, that means to publish
 				 * everything; so if we saw a publication that includes all
 				 * columns, skip this.
 				 */
@@ -1332,14 +1331,15 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 						if (isnull)
 						{
 							/*
-							 * If we see a publication with no columns, reset the
+							 * If we see a publication with no column filter, it
+							 * means we need to publish all columns, so reset the
 							 * list and ignore further ones.
 							 */
 							all_columns = true;
 							bms_free(entry->columns);
 							entry->columns = NULL;
 						}
-						else if (!isnull)
+						else
 						{
 							ArrayType  *arr;
 							int			nelems;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2c347e80ee0..2f29e0d7189 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4160,7 +4160,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 				appendPQExpBufferStr(attribs, fmtId(attnames[k]));
 			}
-			pubrinfo[i].pubrattrs = attribs->data;
+			pubrinfo[j].pubrattrs = attribs->data;
 		}
 		else
 			pubrinfo[j].pubrattrs = NULL;
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 70f73d01dc0..ff9d9a43984 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -86,6 +86,8 @@ typedef struct Publication
 typedef struct PublicationRelInfo
 {
 	Relation	relation;
+
+	/* List of columns to replicate (or NIL to replicate all columns) */
 	List	   *columns;
 } PublicationRelInfo;
 
-- 
2.34.1

From eabd329888df71f18467f6c6be53ae58de3a8d9e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 15 Feb 2022 14:14:40 +0100
Subject: [PATCH 4/4] rework 028_column_list.pl

---
 src/test/subscription/t/028_column_list.pl | 917 ++++++++++++++++++---
 1 file changed, 820 insertions(+), 97 deletions(-)

diff --git a/src/test/subscription/t/028_column_list.pl b/src/test/subscription/t/028_column_list.pl
index 5a4f022f268..8b109402af6 100644
--- a/src/test/subscription/t/028_column_list.pl
+++ b/src/test/subscription/t/028_column_list.pl
@@ -5,7 +5,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 9;
+use Test::More tests => 31;
 
 # setup
 
@@ -21,144 +21,867 @@ $node_subscriber->start;
 
 my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
 
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE tab1 (a int PRIMARY KEY, \"B\" int, c int)");
+# setup tables on both nodes
 
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE tab1 (a int PRIMARY KEY, \"B\" int, c int)");
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE tab2 (a int PRIMARY KEY, b varchar, c int);
-	INSERT INTO tab2 VALUES (2, 'foo', 2);");
-# Test with weird column names
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE tab3 (\"a'\" int PRIMARY KEY, B varchar, \"c'\" int)");
+# tab1: simple 1:1 replication
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab1 (a int PRIMARY KEY, "B" int, c int)
+));
 
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE test_part (a int PRIMARY KEY, b text, c timestamptz) PARTITION BY LIST (a)");
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3)");
-# Test replication with multi-level partition
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (4,5,6) PARTITION BY LIST (a)");
-$node_publisher->safe_psql('postgres',
-	"CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (4,5)");
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab1 (a int PRIMARY KEY, "B" int, c int)
+));
 
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE test_part (a int PRIMARY KEY, b text) PARTITION BY LIST (a)");
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3)");
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE tab3 (\"a'\" int PRIMARY KEY, \"c'\" int)");
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE tab2 (a int PRIMARY KEY, b varchar)");
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (4,5,6) PARTITION BY LIST (a)");
-$node_subscriber->safe_psql('postgres',
-	"CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (4,5)");
+# tab2: replication from regular to table with fewer columns
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab2 (a int PRIMARY KEY, b varchar, c int);
+));
 
-# Test create publication with a column list
-$node_publisher->safe_psql('postgres',
-	"CREATE PUBLICATION pub1 FOR TABLE tab1(a, \"B\"), tab3(\"a'\",\"c'\"), test_part(a,b)");
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab2 (a int PRIMARY KEY, b varchar)
+));
+
+# tab3: simple 1:1 replication with weird column names
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab3 ("a'" int PRIMARY KEY, "B" varchar, "c'" int)
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab3 ("a'" int PRIMARY KEY, "c'" int)
+));
+
+# test_part: partitioned tables, with partitioning (including multi-level
+# partitioning, and fewer columns on the subscriber)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part (a int PRIMARY KEY, b text, c timestamptz) PARTITION BY LIST (a);
+	CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3);
+	CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (4,5,6) PARTITION BY LIST (a);
+	CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (4,5);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part (a int PRIMARY KEY, b text) PARTITION BY LIST (a);
+	CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3);
+	CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (4,5,6) PARTITION BY LIST (a);
+	CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (4,5);
+));
+
+# tab4: table with user-defined enum types
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TYPE test_typ AS ENUM ('blue', 'red');
+	CREATE TABLE tab4 (a INT PRIMARY KEY, b test_typ, c int, d text);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TYPE test_typ AS ENUM ('blue', 'red');
+	CREATE TABLE tab4 (a INT PRIMARY KEY, b test_typ, d text);
+));
+
+
+# TEST: create publication and subscription for some of the tables with
+# column lists
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub1
+	FOR TABLE tab1 (a, "B"), tab3 ("a'", "c'"), test_part (a, b), tab4 (a, b, d);
+));
+
+# check that we got the right prattrs values for the publication in the
+# pg_publication_rel catalog (order by relname, to get stable ordering)
+my $result = $node_publisher->safe_psql('postgres', qq(
+	SELECT relname, prattrs
+	FROM pg_publication_rel pb JOIN pg_class pc ON(pb.prrelid = pc.oid)
+	ORDER BY relname
+));
 
-my $result = $node_publisher->safe_psql('postgres',
-	"select relname, prattrs from pg_publication_rel pb, pg_class pc where pb.prrelid = pc.oid;");
 is($result, qq(tab1|1 2
 tab3|1 3
+tab4|1 2 4
 test_part|1 2), 'publication relation updated');
 
-$node_subscriber->safe_psql('postgres',
-	"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
-);
-# Initial sync
+# create subscription for the publication, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1
+));
+
 $node_publisher->wait_for_catchup('sub1');
 
-$node_publisher->safe_psql('postgres',
-	"INSERT INTO tab1 VALUES (1,2,3)");
+# TEST: insert data into the tables, and see we got replication of just
+# the filtered columns
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO tab1 VALUES (4, 5, 6);
+));
 
-$node_publisher->safe_psql('postgres',
-	"INSERT INTO tab3 VALUES (1,2,3)");
-# Test for replication of partition data
-$node_publisher->safe_psql('postgres',
-	"INSERT INTO test_part VALUES (1,'abc', '2021-07-04 12:00:00')");
-$node_publisher->safe_psql('postgres',
-	"INSERT INTO test_part VALUES (2,'bcd', '2021-07-03 11:12:13')");
-# Test for replication of multi-level partition data
-$node_publisher->safe_psql('postgres',
-	"INSERT INTO test_part VALUES (4,'abc', '2021-07-04 12:00:00')");
-$node_publisher->safe_psql('postgres',
-	"INSERT INTO test_part VALUES (5,'bcd', '2021-07-03 11:12:13')");
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab3 VALUES (1, 2, 3);
+	INSERT INTO tab3 VALUES (4, 5, 6);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab4 VALUES (1, 'red', 3, 'oh my');
+	INSERT INTO tab4 VALUES (2, 'blue', 4, 'hello');
+));
+
+# replication of partitioned table
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part VALUES (1, 'abc', '2021-07-04 12:00:00');
+	INSERT INTO test_part VALUES (2, 'bcd', '2021-07-03 11:12:13');
+	INSERT INTO test_part VALUES (4, 'abc', '2021-07-04 12:00:00');
+	INSERT INTO test_part VALUES (5, 'bcd', '2021-07-03 11:12:13');
+));
+
+# wait for catchup before checking the subscriber
+$node_publisher->wait_for_catchup('sub1');
 
+# tab1: only (a,b) is replicated
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM tab1");
-is($result, qq(1|2|), 'insert on column tab1.c is not replicated');
+is($result, qq(1|2|
+4|5|), 'insert on column tab1.c is not replicated');
 
+# tab3: only (a,c) is replicated
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM tab3");
-is($result, qq(1|3), 'insert on column tab3.b is not replicated');
+is($result, qq(1|3
+4|6), 'insert on column tab3.b is not replicated');
+
+# tab4: only (a,b,d) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab4");
+is($result, qq(1|red|oh my
+2|blue|hello), 'insert on column tab4.c is not replicated');
 
+# test_part: (a,b) is replicated
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM test_part");
-is($result, qq(1|abc\n2|bcd\n4|abc\n5|bcd), 'insert on all columns is replicated');
+is($result, qq(1|abc
+2|bcd
+4|abc
+5|bcd), 'insert on column test_part.c columns is not replicated');
+
+# TEST: do some updated on some of the tables, both on columns included
+# in the column list and other
 
+# tab1: update of replicated column
 $node_publisher->safe_psql('postgres',
-	"UPDATE tab1 SET c = 5 where a = 1");
+	qq(UPDATE tab1 SET "B" = 2 * "B" where a = 1));
 
-$node_publisher->wait_for_catchup('sub1');
+# tab1: update of non-replicated column
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab1 SET c = 2*c where a = 4));
 
-$result = $node_subscriber->safe_psql('postgres',
-	"SELECT * FROM tab1");
-is($result, qq(1|2|), 'update on column tab1.c is not replicated');
+# tab3: update of non-replicated
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab3 SET "B" = "B" || ' updated' where "a'" = 4));
 
-# Verify user-defined types
+# tab3: update of replicated column
 $node_publisher->safe_psql('postgres',
-	qq{CREATE TYPE test_typ AS ENUM ('blue', 'red');
-	CREATE TABLE test_tab4 (a INT PRIMARY KEY, b test_typ, c int, d text);
-	ALTER PUBLICATION pub1 ADD TABLE test_tab4 (a, b, d);
-	});
-$node_subscriber->safe_psql('postgres',
-	qq{CREATE TYPE test_typ AS ENUM ('blue', 'red');
-	CREATE TABLE test_tab4 (a INT PRIMARY KEY, b test_typ, d text);
-	});
+	qq(UPDATE tab3 SET "c'" = 2 * "c'" where "a'" = 1));
+
+# tab4
 $node_publisher->safe_psql('postgres',
-	"INSERT INTO test_tab4 VALUES (1, 'red', 3, 'oh my');");
+	qq(UPDATE tab4 SET b = 'blue', c = c * 2, d = d || ' updated' where a = 1));
 
-# Test alter publication with a column list
+# tab4
 $node_publisher->safe_psql('postgres',
-	"ALTER PUBLICATION pub1 ADD TABLE tab2(a, b)");
+	qq(UPDATE tab4 SET b = 'red', c = c * 2, d = d || ' updated' where a = 2));
 
-$node_subscriber->safe_psql('postgres',
-	"ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION"
-);
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab1 ORDER BY a));
+is($result,
+qq(1|4|
+4|5|), 'only update on column tab1.b is replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab3 ORDER BY "a'"));
+is($result,
+qq(1|6
+4|6), 'only update on column tab3.c is replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab4 ORDER BY a));
+
+is($result, qq(1|blue|oh my updated
+2|red|hello updated), 'update on column tab4.c is not replicated');
+
+
+# TEST: add table with a column list, insert data, replicate
+
+# insert some data before adding it to the publication
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab2 VALUES (1, 'abc', 3);
+));
 
 $node_publisher->safe_psql('postgres',
-	"INSERT INTO tab2 VALUES (1,'abc',3)");
-$node_publisher->safe_psql('postgres',
-	"UPDATE tab2 SET c = 5 where a = 2");
+	"ALTER PUBLICATION pub1 ADD TABLE tab2 (a, b)");
+
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION");
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab2 VALUES (2, 'def', 6);
+));
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
-	"SELECT * FROM tab2 WHERE a = 1");
-is($result, qq(1|abc), 'insert on column tab2.c is not replicated');
+	"SELECT * FROM tab2 ORDER BY a");
+is($result, qq(1|abc
+2|def), 'insert on column tab2.c is not replicated');
 
-$result = $node_subscriber->safe_psql('postgres',
-	"SELECT * FROM tab2 WHERE a = 2");
-is($result, qq(2|foo), 'update on column tab2.c is not replicated');
+$node_publisher->safe_psql('postgres', qq(
+	UPDATE tab2 SET c = 5 where a = 1;
+	UPDATE tab2 SET b = 'xyz' where a = 2;
+));
+
+$node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
-	"SELECT * FROM test_tab4");
-is($result, qq(1|red|oh my), 'insert on table with user-defined type');
-
-$node_publisher->safe_psql('postgres', "CREATE TABLE tab5 (a int PRIMARY KEY, b int, c int, d int)");
-$node_subscriber->safe_psql('postgres', "CREATE TABLE tab5 (a int PRIMARY KEY, b int, d int)");
-$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub2 FOR TABLE tab5 (a, b)");
-$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub3 FOR TABLE tab5 (a, d)");
-$node_subscriber->safe_psql('postgres',    "CREATE SUBSCRIPTION sub2 CONNECTION '$publisher_connstr' PUBLICATION pub2, pub3");
-$node_publisher->wait_for_catchup('sub2');
+	"SELECT * FROM tab2 ORDER BY a");
+is($result, qq(1|abc
+2|xyz), 'update on column tab2.c is not replicated');
+
+# TEST: add a table to two publications with different column lists, and
+# create a single subscription replicating both publications
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab5 (a int PRIMARY KEY, b int, c int, d int);
+	CREATE PUBLICATION pub2 FOR TABLE tab5 (a, b);
+	CREATE PUBLICATION pub3 FOR TABLE tab5 (a, d);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab5 (a int PRIMARY KEY, b int, d int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub2, pub3
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+# TEST: insert data and make sure all the columns (union of the columns lists)
+# were replicated
 $node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (1, 11, 111, 1111)");
 $node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (2, 22, 222, 2222)");
-$node_publisher->wait_for_catchup('sub2');
-is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab5;"),
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab5 ORDER BY a"),
    qq(1|11|1111
 2|22|2222),
    'overlapping publications with overlapping column lists');
+
+# and finally, set the column filter to ALL for one of the publications,
+# which means replicating all columns (removing the column filter), but
+# first add the missing column to the table on subscriber
+$node_publisher->safe_psql('postgres', qq(
+	ALTER PUBLICATION pub3 ALTER TABLE tab5 SET COLUMNS ALL;
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN c INT;
+));
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (3, 33, 333, 3333)");
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab5 ORDER BY a"),
+   qq(1|11|1111|
+2|22|2222|
+3|33|3333|333),
+   'overlapping publications with overlapping column lists');
+
+# TEST: create a table with a column filter, then change the replica
+# identity by replacing a primary key (but use a different column in
+# the column filter)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab6 (a int PRIMARY KEY, b int, c int, d int);
+	CREATE PUBLICATION pub4 FOR TABLE tab6 (a, b);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab6 (a int PRIMARY KEY, b int, c int, d int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub4
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab6 VALUES (1, 22, 333, 4444);
+	UPDATE tab6 SET b = b * 2, c = c * 3, d = d * 4 WHERE a = 1;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab6 ORDER BY a"),
+   "1|44||", 'replication with the original primary key');
+
+# now redefine the constraint - move the primary key to a different column
+# (which is still covered by the column list, though)
+
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab6 DROP CONSTRAINT tab6_pkey;
+	ALTER TABLE tab6 ADD PRIMARY KEY (b);
+));
+
+# we need to do the same thing on the subscriber
+# XXX What would happen if this happens before the publisher ALTER? Or
+# interleaved, somehow? But that seems unrelated to column lists.
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER TABLE tab6 DROP CONSTRAINT tab6_pkey;
+	ALTER TABLE tab6 ADD PRIMARY KEY (b);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab6 VALUES (2, 55, 666, 8888);
+	UPDATE tab6 SET b = b * 2, c = c * 3, d = d * 4 WHERE a = 2;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab6 ORDER BY a"),
+   qq(1|44||
+2|110||),
+   'replication with the modified primary key');
+
+# now switch the primary key back, but also generate writes between the
+# drop and creation of the new constraint
+
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab6 DROP CONSTRAINT tab6_pkey;
+	INSERT INTO tab6 VALUES (3, 33, 999, 7777);
+	-- update/delete is not allowed for tables without RI
+	ALTER TABLE tab6 ADD PRIMARY KEY (a);
+	UPDATE tab6 SET b = b * 2, c = c * 3, d = d * 4 WHERE a = 3;
+	DELETE FROM tab6 WHERE a = 1;
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER TABLE tab6 DROP CONSTRAINT tab6_pkey;
+	ALTER TABLE tab6 ADD PRIMARY KEY (a);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab6 ORDER BY a"),
+   qq(2|110||
+3|66||),
+   'replication with the modified primary key');
+
+
+# TEST: create a table with a column filter, then change the replica
+# identity by replacing a primary key with a key on multiple columns
+# (all of them covered by the column filter)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab7 (a int PRIMARY KEY, b int, c int, d int);
+	CREATE PUBLICATION pub5 FOR TABLE tab7 (a, b);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab7 (a int PRIMARY KEY, b int, c int, d int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub5
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab7 VALUES (1, 22, 333, 4444);
+	UPDATE tab7 SET b = b * 2, c = c * 3, d = d * 4 WHERE a = 1;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab7 ORDER BY a"),
+   "1|44||", 'replication with the original primary key');
+
+# now redefine the constraint - move the primary key to a different column
+# (which is not covered by the column list)
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab7 DROP CONSTRAINT tab7_pkey;
+	ALTER TABLE tab7 ADD PRIMARY KEY (a, b);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab7 VALUES (2, 55, 666, 8888);
+	UPDATE tab7 SET b = b * 2, c = c * 3, d = d * 4 WHERE a = 2;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab7 ORDER BY a"),
+   qq(1|44||
+2|110||),
+   'replication with the modified primary key');
+
+# now switch the primary key again to another columns not covered by the
+# column filter, but also generate writes between the drop and creation
+# of the new constraint
+
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab7 DROP CONSTRAINT tab7_pkey;
+	INSERT INTO tab7 VALUES (3, 33, 999, 7777);
+	-- update/delete is not allowed for tables without RI
+	ALTER TABLE tab7 ADD PRIMARY KEY (b, a);
+	UPDATE tab7 SET b = b * 2, c = c * 3, d = d * 4 WHERE a = 3;
+	DELETE FROM tab7 WHERE a = 1;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab7 ORDER BY a"),
+   qq(2|110||
+3|66||),
+   'replication with the modified primary key');
+
+
+# TEST: partitioned tables (with publish_via_partition_root = false)
+# and replica identity. The (leaf) partitions may have different RI, so
+# we need to check the partition RI (with respect to the column filter)
+# while attaching the partition.
+
+# First, let's create a partitioned table with two partitions, each with
+# a different RI, but a column filter not covering all those RI.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_a (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_a_1 PARTITION OF test_part_a FOR VALUES IN (1);
+	ALTER TABLE test_part_a_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_a_1 REPLICA IDENTITY USING INDEX test_part_a_1_pkey;
+
+	CREATE TABLE test_part_a_2 PARTITION OF test_part_a FOR VALUES IN (2);
+	ALTER TABLE test_part_a_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_a_2 REPLICA IDENTITY USING INDEX test_part_a_2_pkey;
+));
+
+# do the same thing on the subscriber
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_a (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_a_1 PARTITION OF test_part_a FOR VALUES IN (1);
+	ALTER TABLE test_part_a_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_a_1 REPLICA IDENTITY USING INDEX test_part_a_1_pkey;
+
+	CREATE TABLE test_part_a_2 PARTITION OF test_part_a FOR VALUES IN (2);
+	ALTER TABLE test_part_a_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_a_2 REPLICA IDENTITY USING INDEX test_part_a_2_pkey;
+));
+
+# create a publication replicating just the column "a", which is not enough
+# for the second partition
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub6 FOR TABLE test_part_a (a) WITH (publish_via_partition_root = false);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub6
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_a VALUES (1, 1);
+	-- FIXME: This fails, because it does not send (b) as needed for the RI
+	-- INSERT INTO test_part_a VALUES (2, 2);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_a ORDER BY a, b"),
+   qq(1|1
+2|2),
+   'partitions with different replica identities not replicated correctly');
+
+
+# This time start with a column filter covering RI for all partitions, but
+# then update the column filter to not cover column "b" (needed by the
+# second partition)
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_b (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_b_1 PARTITION OF test_part_b FOR VALUES IN (1,3);
+	ALTER TABLE test_part_b_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_b_1 REPLICA IDENTITY USING INDEX test_part_b_1_pkey;
+
+	CREATE TABLE test_part_b_2 PARTITION OF test_part_b FOR VALUES IN (2,4);
+	ALTER TABLE test_part_b_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_b_2 REPLICA IDENTITY USING INDEX test_part_b_2_pkey;
+));
+
+# do the same thing on the subscriber
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_b (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_b_1 PARTITION OF test_part_b FOR VALUES IN (1,3);
+	ALTER TABLE test_part_b_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_b_1 REPLICA IDENTITY USING INDEX test_part_b_1_pkey;
+
+	CREATE TABLE test_part_b_2 PARTITION OF test_part_b FOR VALUES IN (2,4);
+	ALTER TABLE test_part_b_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_b_2 REPLICA IDENTITY USING INDEX test_part_b_2_pkey;
+));
+
+# create a publication replicating both columns, which is sufficient for
+# both partitions
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub7 FOR TABLE test_part_b (a, b) WITH (publish_via_partition_root = false);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub7
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_b VALUES (1, 1);
+	INSERT INTO test_part_b VALUES (2, 2);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_b ORDER BY a, b"),
+   qq(1|1
+2|2),
+   'partitions with different replica identities not replicated correctly');
+
+# now alter the publication to only replicate column "a"
+$node_publisher->safe_psql('postgres', qq(
+	ALTER PUBLICATION pub7 ALTER TABLE test_part_b SET COLUMNS (a);
+));
+
+# now retry the inserts
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_b VALUES (3, 2);
+	-- FIXME: This fails, because it does not send (b) as needed for the RI
+	-- INSERT INTO test_part_b VALUES (4, 3);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_b ORDER BY a, b"),
+   qq(1|1
+2|2
+3|2
+4|3),
+   'partitions with different replica identities not replicated correctly');
+
+
+# TEST: This time start with a column filter covering RI for all partitions,
+# but then update RI for one of the partitions to not be covered by the
+# column filter anymore.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_c (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_c_1 PARTITION OF test_part_c FOR VALUES IN (1,3);
+	ALTER TABLE test_part_c_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_c_1 REPLICA IDENTITY USING INDEX test_part_c_1_pkey;
+
+	CREATE TABLE test_part_c_2 PARTITION OF test_part_c FOR VALUES IN (2,4);
+	ALTER TABLE test_part_c_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_c_2 REPLICA IDENTITY USING INDEX test_part_c_2_pkey;
+));
+
+# do the same thing on the subscriber
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_c (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_c_1 PARTITION OF test_part_c FOR VALUES IN (1,3);
+	ALTER TABLE test_part_c_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_c_1 REPLICA IDENTITY USING INDEX test_part_c_1_pkey;
+
+	CREATE TABLE test_part_c_2 PARTITION OF test_part_c FOR VALUES IN (2,4);
+	ALTER TABLE test_part_c_2 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_c_2 REPLICA IDENTITY USING INDEX test_part_c_2_pkey;
+));
+
+# create a publication replicating both columns, which is sufficient for
+# both partitions
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub8 FOR TABLE test_part_c (a) WITH (publish_via_partition_root = false);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub8
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_c VALUES (1, 1);
+	INSERT INTO test_part_c VALUES (2, 2);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_c ORDER BY a, b"),
+   qq(1|1
+2|2),
+   'partitions with different replica identities not replicated correctly');
+
+# now alter the publication to only replicate column "a"
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE test_part_c_2 DROP CONSTRAINT test_part_c_2_pkey;
+	ALTER TABLE test_part_c_2 ADD PRIMARY KEY (b);
+));
+
+# now retry the inserts
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_c VALUES (3, 2);
+	-- FIXME: This fails, because it does not send (b) as needed for the RI
+	-- INSERT INTO test_part_c VALUES (4, 3);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_c ORDER BY a, b"),
+   qq(1|1
+2|2
+3|2
+4|3),
+   'partitions with different replica identities not replicated correctly');
+
+
+# TEST: Start with a single partition, with RI compatible with the column
+# filter, and then attach a partition with incompatible RI.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_d (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_d_1 PARTITION OF test_part_d FOR VALUES IN (1,3);
+	ALTER TABLE test_part_d_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_d_1 REPLICA IDENTITY USING INDEX test_part_d_1_pkey;
+));
+
+# do the same thing on the subscriber (in fact, create both partitions right
+# away, no need to delay that)
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_d (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_d_1 PARTITION OF test_part_d FOR VALUES IN (1,3);
+	ALTER TABLE test_part_d_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_d_1 REPLICA IDENTITY USING INDEX test_part_d_1_pkey;
+
+	CREATE TABLE test_part_d_2 PARTITION OF test_part_d FOR VALUES IN (2,4);
+	ALTER TABLE test_part_d_2 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_d_2 REPLICA IDENTITY USING INDEX test_part_d_2_pkey;
+));
+
+# create a publication replicating both columns, which is sufficient for
+# both partitions
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub9 FOR TABLE test_part_d (a) WITH (publish_via_partition_root = false);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub9
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_d VALUES (1, 1);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_d ORDER BY a, b"),
+   qq(1|1),
+   'partitions with different replica identities not replicated correctly');
+
+# now add the second partition, with a mismatching RI
+# FIXME this should fail, complaining about the column filter
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_d_2 PARTITION OF test_part_d FOR VALUES IN (2,4);
+	ALTER TABLE test_part_d_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_d_2 REPLICA IDENTITY USING INDEX test_part_d_2_pkey;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_d ORDER BY a, b"),
+   qq(1|1
+2|2
+3|2
+4|3),
+   'partitions with different replica identities not replicated correctly');
+
+
+# TEST: With a table included in multiple publications, we should use a
+# union of the column filters. So with column filters (a,b) and (a,c) we
+# should replicate (a,b,c).
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c int);
+	CREATE PUBLICATION pub_mix_1 FOR TABLE test_mix_1 (a, b);
+	CREATE PUBLICATION pub_mix_2 FOR TABLE test_mix_1 (a, c);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c int);
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub_mix_1, pub_mix_2;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_mix_1 VALUES (1, 2, 3);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_1"),
+   qq(1|2|3),
+   'a mix of publications should use a union of column filter');
+
+
+# TEST: With a table included in multiple publications, we should use a
+# union of the column filters. If any of the publications is FOR ALL
+# TABLES, we should replicate all columns.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_2 (a int PRIMARY KEY, b int, c int);
+	CREATE PUBLICATION pub_mix_3 FOR TABLE test_mix_2 (a, b);
+	CREATE PUBLICATION pub_mix_4 FOR ALL TABLES;
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_2 (a int PRIMARY KEY, b int, c int);
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub_mix_3, pub_mix_4;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_mix_2 VALUES (1, 2, 3);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_2"),
+   qq(1|2|3),
+   'a mix of publications should use a union of column filter');
+
+
+# TEST: With a table included in multiple publications, we should use a
+# union of the column filters. If any of the publications is FOR ALL
+# TABLES IN SCHEMA, we should replicate all columns.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_3 (a int PRIMARY KEY, b int, c int);
+	CREATE PUBLICATION pub_mix_5 FOR TABLE test_mix_3 (a, b);
+	CREATE PUBLICATION pub_mix_6 FOR ALL TABLES IN SCHEMA public;
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_3 (a int PRIMARY KEY, b int, c int);
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub_mix_5, pub_mix_6;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_mix_3 VALUES (1, 2, 3);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_3"),
+   qq(1|2|3),
+   'a mix of publications should use a union of column filter');
+
+
+# TEST: Check handling of publish_via_partition_root - if a partition is
+# not published through partition root, we should not apply the column
+# filter defined for the whole table - both during the initial sync and
+# when replicating changes. This is what we do for row filters.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_root (a int PRIMARY KEY, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE test_root_1 PARTITION OF test_root FOR VALUES FROM (1) TO (10);
+	CREATE TABLE test_root_2 PARTITION OF test_root FOR VALUES FROM (10) TO (20);
+
+	CREATE PUBLICATION pub_root_true FOR TABLE test_root (a) WITH (publish_via_partition_root = true);
+	CREATE PUBLICATION pub_root_false FOR TABLE test_root (a) WITH (publish_via_partition_root = false);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_root (a int PRIMARY KEY, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE test_root_1 PARTITION OF test_root FOR VALUES FROM (1) TO (10);
+	CREATE TABLE test_root_2 PARTITION OF test_root FOR VALUES FROM (10) TO (20);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub_root_true;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_root VALUES (1, 2, 3);
+	INSERT INTO test_root VALUES (10, 20, 30);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_root ORDER BY a, b, c"),
+   qq(1||
+10||),
+   'publication via partition root applies column filter');
+
+# now switch to using the publication with (publish_via_partition_root = false),
+# which should not apply the column filter
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 DROP PUBLICATION pub_root_true;
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub_root_false;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_root VALUES (2, 4, 6);
+	INSERT INTO test_root VALUES (11, 22, 33);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_root ORDER BY a, b, c"),
+   qq(1||
+2|4|6
+10||
+11|22|33),
+   'publication via partition root applies column filter');
+
+# and finally add both publications to the subscription - in this case we
+# should not apply the column filter either, because the "false" case is
+# handled as column filter with all columns
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 ADD PUBLICATION pub_root_true;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_root VALUES (3, 6, 9);
+	INSERT INTO test_root VALUES (12, 24, 36);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_root ORDER BY a, b, c"),
+   qq(1||
+2|4|6
+3|6|9
+10||
+11|22|33
+12|24|36),
+   'publication via partition root applies column filter');
-- 
2.34.1

---

From: Amit Kapila <[email-masked]>
Date: Wed, 16 Feb 2022 15:35:34 +0530

> publication, attaching a partition or changing a column filter.

I feel it is better to follow the way described by Peter E. here [1]
to handle these cases. The row filter patch is also using the same
scheme as that is what we are doing now for Updates/Deletes and it
would be really challenging and much more effort/code to deal with
everything at DDL time. I have tried to explain some of that in my
emails [2][3].

[1] - https://www.postgresql.org/message-id/ca91dc91-80ba-e954-213e-b4170a6160f5%40enterprisedb.com
[2] - https://www.postgresql.org/message-id/CAA4eK1%2Bm45Xyzx7AUY9TyFnB6CZ7_%2B_uooPb7WHSpp7UE%3DYmKg%40mail.gmail.com
[3] - https://www.postgresql.org/message-id/CAA4eK1%2B1DMkCip9SB3B0_u0Q6fGf-D3vgqQodkLfur0qkL482g%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 2 Mar 2022 13:13:15 +0100

Hi,

Attached is an updated patch, addressing most of the issues reported so
far. There are various minor tweaks, but the main changes are:

1) regular regression tests, verifying (hopefully) all the various cases
of publication vs. column filters, replica identity check at various
changes and so on

2) pg_dump tests, testing column filters (alone and with row filter)

3) checks of column filter vs. publish_via_partition_root and replica
identity, following the same logic as the row-filter patch (hopefully,
it touches the same places, using the same logic, ...)

That means - with "publish_via_partition_root=false" it's not allowed to
specify column filters on partitioned tables, only for leaf partitions.

And we check column filter vs. replica identity when adding tables to
publications, or whenever we change the replica identity.

The patch is still a bit crude, I'm sure some of the places (especially
the new ones) may need cleanup/recovery. But I think it's much closer to
being committable, I think.

The first two simple patches are adding tests for the row filtering. So
this is not really part of this patch.

regards

From e781f840e38701c63d8b57ff36bd520f2cced6ad Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Sat, 26 Feb 2022 17:33:09 +0100
Subject: [PATCH 1/3] Verify changing WHERE condition for a publication

Commit 52e4f0cd47 added support for row filters in logical replication,
including regression tests with multiple ALTER PUBLICATION commands,
modifying the row filter. But the tests never verified that the row
filter was actually updated in the catalog. This adds a couple \d and
\dRp commands, to verify the catalog was updated.
---
 src/test/regress/expected/publication.out | 66 +++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  8 +++
 2 files changed, 74 insertions(+)

diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 3c382e520e4..227ce759486 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -395,15 +395,81 @@ LINE 1: ...ICATION testpub5 ADD TABLE testpub_rf_tbl1 WHERE (b < '2' CO...
 DETAIL:  User-defined collations are not allowed.
 -- ok - NULLIF is allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (NULLIF(1,2) = a);
+\d+ testpub_rf_tbl1
+                              Table "public.testpub_rf_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           |          |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+Publications:
+    "testpub5" WHERE (NULLIF(1, 2) = a)
+
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | f         | f
+Tables:
+    "public.testpub_rf_tbl1" WHERE (NULLIF(1, 2) = a)
+
 -- ok - built-in operators are allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (a IS NULL);
+\d+ testpub_rf_tbl1
+                              Table "public.testpub_rf_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           |          |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+Publications:
+    "testpub5" WHERE (a IS NULL)
+
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | f         | f
+Tables:
+    "public.testpub_rf_tbl1" WHERE (a IS NULL)
+
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE ((a > 5) IS FALSE);
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (a IS DISTINCT FROM 5);
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE ((a, a + 1) < (2, 3));
 -- ok - built-in type coercions between two binary compatible datatypes are allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (b::varchar < '2');
+\d+ testpub_rf_tbl1
+                              Table "public.testpub_rf_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           |          |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+Publications:
+    "testpub5" WHERE (((b)::character varying)::text < '2'::text)
+
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | f         | f
+Tables:
+    "public.testpub_rf_tbl1" WHERE (((b)::character varying)::text < '2'::text)
+
 -- ok - immutable built-in functions are allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl4 WHERE (length(g) < 6);
+\d+ testpub_rf_tbl1
+                              Table "public.testpub_rf_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           |          |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | f         | f
+Tables:
+    "public.testpub_rf_tbl4" WHERE (length(g) < 6)
+
 -- fail - user-defined types are not allowed
 CREATE TYPE rf_bug_status AS ENUM ('new', 'open', 'closed');
 CREATE TABLE rf_bug (id serial, description text, status rf_bug_status);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 3f04d34264a..cd7e0182716 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -203,15 +203,23 @@ CREATE COLLATION user_collation FROM "C";
 ALTER PUBLICATION testpub5 ADD TABLE testpub_rf_tbl1 WHERE (b < '2' COLLATE user_collation);
 -- ok - NULLIF is allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (NULLIF(1,2) = a);
+\d+ testpub_rf_tbl1
+\dRp+ testpub5
 -- ok - built-in operators are allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (a IS NULL);
+\d+ testpub_rf_tbl1
+\dRp+ testpub5
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE ((a > 5) IS FALSE);
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (a IS DISTINCT FROM 5);
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE ((a, a + 1) < (2, 3));
 -- ok - built-in type coercions between two binary compatible datatypes are allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl1 WHERE (b::varchar < '2');
+\d+ testpub_rf_tbl1
+\dRp+ testpub5
 -- ok - immutable built-in functions are allowed
 ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl4 WHERE (length(g) < 6);
+\d+ testpub_rf_tbl1
+\dRp+ testpub5
 -- fail - user-defined types are not allowed
 CREATE TYPE rf_bug_status AS ENUM ('new', 'open', 'closed');
 CREATE TABLE rf_bug (id serial, description text, status rf_bug_status);
-- 
2.34.1

From 8ee67dd52a1fc08837aa85979dfc0842cc968012 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 1 Mar 2022 15:25:56 +0100
Subject: [PATCH 2/3] Test publication row filters in pg_dump tests

Commit 52e4f0cd47 added support for row filters when replicating tables,
but the commit added no pg_dump tests for this feature. So add at least
a simple test.
---
 src/bin/pg_dump/t/002_pg_dump.pl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index dd065c758fa..c3bcef8c0ec 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2407,12 +2407,12 @@ my %tests = (
 		},
 	},
 
-	'ALTER PUBLICATION pub1 ADD TABLE test_second_table' => {
+	'ALTER PUBLICATION pub1 ADD TABLE test_second_table WHERE (col1 = 1)' => {
 		create_order => 52,
 		create_sql =>
-		  'ALTER PUBLICATION pub1 ADD TABLE dump_test.test_second_table;',
+		  'ALTER PUBLICATION pub1 ADD TABLE dump_test.test_second_table WHERE (col1 = 1);',
 		regexp => qr/^
-			\QALTER PUBLICATION pub1 ADD TABLE ONLY dump_test.test_second_table;\E
+			\QALTER PUBLICATION pub1 ADD TABLE ONLY dump_test.test_second_table WHERE ((col1 = 1));\E
 			/xm,
 		like => { %full_runs, section_post_data => 1, },
 		unlike => { exclude_dump_test_schema => 1, },
-- 
2.34.1

From 223a3c0cbbc24814c2a207314e3d5cd8257b9fba Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 23 Feb 2022 21:15:18 +0100
Subject: [PATCH 3/3] Allow specifying column filters for logical replication

This allows specifying an optional column list when adding a table to
logical replication. Columns not included on this list are not sent to
the subscriber. The filter is specified as a list of columns after the
table name, enclosed in parentheses.

For UPDATE/DELETE publications, the column filter needs to cover all
REPLICA IDENTITY columns. For INSERT publications, the column list is
arbitrary and may omit some REPLICA IDENTITY columns. Furthermore, if
the table uses REPLICA IDENTITY FULL, column filter is not allowed.

The column filter can contain only simple column references. Complex
expressions, function calls etc. are not allowed. This restriction could
be relaxed in the future.

During the initial table synchronization, only columns specified in the
column filter are copied to the subscriber. If the subscription has
several publications, containing the same table with different column
filters, columns specified in any of the filters will be copied. This
means all columns are replicated if the table has no column filter at
all (which is treated as column filter with all columns), of when of the
publications is defined as FOR ALL TABLES (possibly IN SCHEMA for the
schema of the table).

For partitioned tables, publish_via_partition_root determines whether
the row filter for the root or leaf relation will be used. If the
parameter is 'false' (the default), the filter defined for the leaf
relation is used. Otherwise, the column filter for the root partition
will be used.

Psql commands \dRp+ and \d <table-name> now display any column filters.

Author: Rahila Syed <[email-masked]>
Discussion: https://postgr.es/m/CAH2L28vddB_NFdRVpuyRBJEBWjz4BSyTB=[email-masked]
---
 doc/src/sgml/catalogs.sgml                  |  13 +
 doc/src/sgml/protocol.sgml                  |   4 +-
 doc/src/sgml/ref/alter_publication.sgml     |  23 +-
 doc/src/sgml/ref/create_publication.sgml    |  17 +-
 src/backend/catalog/pg_publication.c        | 397 +++++++++-
 src/backend/commands/publicationcmds.c      | 181 ++++-
 src/backend/commands/tablecmds.c            | 209 ++++-
 src/backend/nodes/copyfuncs.c               |   1 +
 src/backend/nodes/equalfuncs.c              |   1 +
 src/backend/parser/gram.y                   |  60 +-
 src/backend/replication/logical/proto.c     |  55 +-
 src/backend/replication/logical/tablesync.c | 271 ++++++-
 src/backend/replication/pgoutput/pgoutput.c | 121 ++-
 src/bin/pg_dump/pg_dump.c                   |  47 +-
 src/bin/pg_dump/pg_dump.h                   |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl            |  60 ++
 src/bin/psql/describe.c                     |  40 +-
 src/include/catalog/pg_publication.h        |   6 +
 src/include/catalog/pg_publication_rel.h    |   1 +
 src/include/nodes/parsenodes.h              |   4 +-
 src/include/replication/logicalproto.h      |   6 +-
 src/test/regress/expected/publication.out   | 212 +++++
 src/test/regress/sql/publication.sql        | 140 ++++
 src/test/subscription/t/029_column_list.pl  | 836 ++++++++++++++++++++
 24 files changed, 2629 insertions(+), 77 deletions(-)
 create mode 100644 src/test/subscription/t/029_column_list.pl

index 83987a99045..2b61f42b71d 100644
@@ -6263,6 +6263,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        Reference to schema
index 32b75f6c78e..aa6827c977b 100644

-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -64,6 +65,12 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    command retain their previous settings.
   </para>
 
+  <para>
+   The <literal>ALTER TABLE ... SET COLUMNS</literal> variant allows changing
+   the set of columns that are included in the publication.  If a column list
+   is specified, it must include the replica identity columns.
+  </para>
+
   <para>
    The remaining variants change the owner and the name of the publication.
   </para>
@@ -112,6 +119,10 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
       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.
+
+      Optionally, a column list can be specified.  See <xref
+      linkend="sql-createpublication"/> for details.
+
       If the optional <literal>WHERE</literal> clause is specified, rows for
       which the <replaceable class="parameter">expression</replaceable>
       evaluates to false or null will not be published. Note that parentheses
@@ -172,9 +183,15 @@ ALTER PUBLICATION noinsert SET (publish = 'update, delete');
   </para>
 
   <para>
 
   <para>
index 4979b9b646d..fb2d013393b 100644

-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -86,6 +86,13 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
       <literal>TRUNCATE</literal> commands.
      </para>
 
+     <para>
@@ -327,6 +334,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 
index 25998fbb39b..840f0a0eb24 100644
@@ -45,12 +45,22 @@
 
+static void check_publication_columns(Publication *pub, Relation targetrel,
+									  Bitmapset *columns);
+static void publication_translate_columns(Relation targetrel, List *columns,
+										  int *natts, AttrNumber **attrs,
+										  Bitmapset **attset);
+
 /*
- * Check if relation can be in given publication and throws appropriate
- * error if not.
+ * Check if relation can be in given publication and that the column
+ * filter is sensible, and throws appropriate error if not.
+ *
+ * columns is the bitmapset of attribute numbers included in the column list,
+ * or NULL if no column list was specified (i.e. all columns are replicated)
  */
 static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Publication *pub, Relation targetrel,
+							   Bitmapset *columns)
 {
 	/* Must be a regular or partitioned table */
 	if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -82,6 +92,103 @@ check_publication_add_relation(Relation targetrel)
 				 errmsg("cannot add relation \"%s\" to publication",
 						RelationGetRelationName(targetrel)),
 				 errdetail("This operation is not supported for unlogged tables.")));
+
+	/*
+	 * Ensure the column filter is compatible with the replica identity and the
+	 * actions the publication is replicating.
+	 */
+	check_publication_columns(pub, targetrel, columns);
+}
+
+/*
+ * Enforce that the column list can only leave out columns that don't
+ * need to be sent as part of replica identity.
+ *
+ * If the relation uses REPLICA IDENTITY FULL, we can't allow any column
+ * list even if it lists all columns of the relation - it'd cause issues
+ * if a column is added later. The column would become part of a replica
+ * identity, violating the rule that the column list includes the whole
+ * replica identity. We could add the column to the column list too, of
+ * course, but it seems rather useles - the column list would always
+ * include all columns, i.e. as if there's no column filter.
+ *
+ * In other cases, the columns in the REPLICA IDENTITY cannot be left out,
+ * except when the publication replicates only inserts. So we check that
+ * for UPDATE/DELETE the replica identity is a subset of the column filter.
+ */
+static void
+check_publication_columns(Publication *pub, Relation targetrel, Bitmapset *columns)
+{
+	List	   *relids;
+	Oid			relid;
+	ListCell   *lc;
+
+	/*
+	 * If there is no column list, we treat it as if the list contains all columns. In
+	 * which case there's nothing to check so we're done.
+	 */
+	if (!columns)
+		return;
+
+	relid = RelationGetRelid(targetrel);
+	relids = find_all_inheritors(relid, NoLock, NULL);
+
+	/* We only care about replica identity on leaf partitions. */
+	foreach(lc, relids)
+	{
+		Oid			partOid = lfirst_oid(lc);
+
+		/* XXX Do we need to lock this? */
+		Relation	partrel = relation_open(partOid, AccessShareLock);
+
+		/*
+		 * ignore non-leaf relations
+		 *
+		 * XXX Should we consider what replica identity is set for the relation?
+		 */
+		if (partrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+			goto cleanup;
+
+		/* With REPLICA IDENTITY FULL no column filter is allowed. */
+		if (partrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("invalid column list for publishing relation \"%s\"",
+						   RelationGetRelationName(targetrel)),	/* XXX maybe report the partition? */
+					errdetail("Cannot specify column list on relations with REPLICA IDENTITY FULL."));
+
+		/* When replicating UPDATE/DELETE, the whole replica identity has to be sent. */
+		if (pub->pubactions.pubupdate || pub->pubactions.pubdelete)
+		{
+			Bitmapset  *idattrs;
+			int			x;
+
+			idattrs = RelationGetIndexAttrBitmap(partrel,
+												 INDEX_ATTR_BITMAP_IDENTITY_KEY);
+
+			/*
+			 * Attnums in the bitmap returned by RelationGetIndexAttrBitmap are
+			 * offset (to handle system columns the usual way), while column filter
+			 * does not use offset, so we can't do bms_is_subset(). Instead, we have
+			 * to loop over the idattrs and check all of them are in the filter.
+			 */
+			x = -1;
+			while ((x = bms_next_member(idattrs, x)) >= 0)
+			{
+				if (!bms_is_member(x + FirstLowInvalidHeapAttributeNumber, columns))
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							errmsg("invalid column list for publishing relation \"%s\"",
+								   RelationGetRelationName(targetrel)),	/* XXX maybe report the partition? */
+							errdetail("All columns in REPLICA IDENTITY must be present in the column list."));
+			}
+
+			bms_free(idattrs);
+		}
+
+cleanup:
+		relation_close(partrel, AccessShareLock);
+	}
 }
 
 /*
@@ -328,6 +435,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	Oid			relid = RelationGetRelid(targetrel);
 	Oid			pubreloid;
 	Publication *pub = GetPublication(pubid);
+	Bitmapset  *attset = NULL;
+	AttrNumber *attarray;
+	int			natts = 0;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
@@ -353,7 +463,18 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
-	check_publication_add_relation(targetrel);
+	/*
+	 * Translate column names to attnums and check the column list is valid.
+	 * We build both a bitmap and array of attnums - array is stored in the
+	 * catalog, while the bitmap is more convenient for checking.
+	 */
+	publication_translate_columns(pri->relation, pri->columns,
+								  &natts, &attarray, &attset);
+
+	check_publication_add_relation(pub, targetrel, attset);
+
+	/* Won't need the bitmapset anymore. */
+	bms_free(attset);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
@@ -367,6 +488,16 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
 
+	if (pri->columns)
+	{
+		int2vector *prattrs;
+
+		prattrs = buildint2vector(attarray, natts);
+		values[Anum_pg_publication_rel_prattrs - 1] = PointerGetDatum(prattrs);
+	}
+	else
+		nulls[Anum_pg_publication_rel_prattrs - 1] = true;
+
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
 		values[Anum_pg_publication_rel_prqual - 1] = CStringGetTextDatum(nodeToString(pri->whereClause));
@@ -382,6 +513,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	/* Register dependencies as needed */
 	ObjectAddressSet(myself, PublicationRelRelationId, pubreloid);
 
@@ -415,6 +554,166 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	return myself;
 }
 
+/*
+ * XXX Should this detect duplicate columns?
@@ -522,6 +821,96 @@ GetRelationPublications(Oid relid)
 	return result;
 }
 
+/*
index 16b8661a1b7..d5fad6c0f79 100644
@@ -608,6 +608,45 @@ TransformPubWhereClauses(List *tables, const char *queryString,
 	}
 }
 
+
+/*
+ * Transform the publication column lists expression for all the relations
+ * in the list.
+ *
+ * XXX The name is a bit misleading, because we don't really transform
+ * anything here - we merely check the column list is compatible with the
+ * definition of the publication (with publish_via_partition_root=false)
+ * we only allow filters on the leaf relations. So maybe rename it?
+ */
+static void
+TransformPubColumnList(List *tables, const char *queryString,
+					   bool pubviaroot)
+{
+	ListCell   *lc;
+
+	foreach(lc, tables)
+	{
+		PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc);
+
+		if (pri->columns == NIL)
+			continue;
+
+		/*
+		 * If the publication doesn't publish changes via the root partitioned
+		 * table, the partition's column filter will be used. So disallow using
+		 * the column list on partitioned table in this case.
+		 */
+		if (!pubviaroot &&
+			pri->relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot use publication column list for relation \"%s\"",
+							RelationGetRelationName(pri->relation)),
+					 errdetail("column list cannot be used for a partitioned table when %s is false.",
+							   "publish_via_partition_root")));
+	}
+}
+
 /*
  * Create new publication.
  */
@@ -724,6 +763,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 			TransformPubWhereClauses(rels, pstate->p_sourcetext,
 									 publish_via_partition_root);
 
+			TransformPubColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
+
 			PublicationAddTables(puboid, rels, true, NULL);
 			CloseTableList(rels);
 		}
@@ -754,6 +796,46 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	return myself;
 }
 
+/*
@@ -838,6 +920,16 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		}
 	}
 
+	/*
+	 * FIXME check pubactions vs. replica identity, to ensure the replica
+	 * identity is included in the column filter. Only do this for update
+	 * and delete publications. See check_publication_columns.
+	 *
+	 * XXX This is needed because publish_via_partition_root may change,
+	 * in which case the row filters may be invalid (e.g. with pvpr=false
+	 * there must be no filter on partitioned tables).
+	 */
+
 	/* Everything ok, form a new tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
@@ -975,10 +1067,20 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 
 		TransformPubWhereClauses(rels, queryString, pubform->pubviaroot);
 
+		TransformPubColumnList(rels, queryString, pubform->pubviaroot);
+
 		PublicationAddTables(pubid, rels, false, stmt);
 	}
 	else if (stmt->action == AP_DropObjects)
 		PublicationDropTables(pubid, rels, false);
+	else if (stmt->action == AP_SetColumns)
+	{
+		Assert(schemaidlist == NIL);
+		Assert(list_length(tables) == 1);
+
+		PublicationSetColumns(stmt, pubform,
+							  linitial_node(PublicationTable, tables));
+	}
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
@@ -991,6 +1093,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 
 		TransformPubWhereClauses(rels, queryString, pubform->pubviaroot);
 
+		TransformPubColumnList(rels, queryString, pubform->pubviaroot);
+
 		/*
 		 * To recreate the relation list for the publication, look for
 		 * existing relations that do not need to be dropped.
@@ -1002,32 +1106,85 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			PublicationRelInfo *oldrel;
 			bool		found = false;
 			HeapTuple	rftuple;
-			bool		rfisnull = true;
 			Node	   *oldrelwhereclause = NULL;
+			Bitmapset  *oldcolumns = NULL;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
 									  ObjectIdGetDatum(oldrelid),
 									  ObjectIdGetDatum(pubid));
 
+			/*
+			 * See if the existing relation currently has a WHERE clause or a
+			 * column filter. We need to compare those too.
+			 */
 			if (HeapTupleIsValid(rftuple))
 			{
+				bool		isnull = true;
 				Datum		whereClauseDatum;
+				Datum		columnListDatum;
 
+				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
 												   Anum_pg_publication_rel_prqual,
-												   &rfisnull);
-				if (!rfisnull)
+												   &isnull);
+				if (!isnull)
 					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
 
+				/* Transform the int2vector column list to a bitmap. */
+				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+												   Anum_pg_publication_rel_prattrs,
+												   &isnull);
+
+				/*
+				 * XXX Maybe make this a separate function. We do this on
+				 * multiple places.
+				 */
+				if (!isnull)
+				{
+					ArrayType  *arr;
+					int			nelems;
+					int16	   *elems;
+
+					arr = DatumGetArrayTypeP(columnListDatum);
+					nelems = ARR_DIMS(arr)[0];
+					elems = (int16 *) ARR_DATA_PTR(arr);
+
+					/* XXX is there a danger of memory leak here? beware */
+					for (int i = 0; i < nelems; i++)
+						oldcolumns = bms_add_member(oldcolumns, elems[i]);
+				}
+
 				ReleaseSysCache(rftuple);
 			}
 
 			foreach(newlc, rels)
 			{
 				PublicationRelInfo *newpubrel;
+				Oid					newrelid;
+				Bitmapset		   *newcolumns = NULL;
 
 				newpubrel = (PublicationRelInfo *) lfirst(newlc);
+				newrelid = RelationGetRelid(newpubrel->relation);
+
+				/*
+				 * If the new publication has column filter, transform it to
+				 * a bitmap too.
+				 */
+				if (newpubrel->columns)
+				{
+					ListCell   *lc;
+
+					foreach(lc, newpubrel->columns)
+					{
+						char	   *colname = strVal(lfirst(lc));
+						AttrNumber	attnum = get_attnum(newrelid, colname);
+
+						/* no checks needed here, that happens elsewhere */
+
+						newcolumns = bms_add_member(newcolumns, attnum);
+					}
+				}
 
 				/*
 				 * Check if any of the new set of relations matches with the
@@ -1037,7 +1194,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 */
 				if (RelationGetRelid(newpubrel->relation) == oldrelid)
 				{
-					if (equal(oldrelwhereclause, newpubrel->whereClause))
+					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
+						bms_equal(oldcolumns, newcolumns))
 					{
 						found = true;
 						break;
@@ -1056,6 +1214,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			{
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
+				oldrel->columns = NIL;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1117,7 +1276,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 	}
 	else if (stmt->action == AP_DropObjects)
 		PublicationDropSchemas(pubform->oid, schemaidlist, false);
-	else						/* AP_SetObjects */
+	else if (stmt->action == AP_SetObjects)
 	{
 		List	   *oldschemaids = GetPublicationSchemas(pubform->oid);
 		List	   *delschemas = NIL;
@@ -1140,6 +1299,10 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		 */
 		PublicationAddSchemas(pubform->oid, schemaidlist, true, stmt);
 	}
+	else
+	{
+		/* Nothing to do for AP_SetColumns */
+	}
 }
 
 /*
@@ -1443,6 +1606,7 @@ OpenTableList(List *tables)
 		pub_rel = palloc(sizeof(PublicationRelInfo));
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
+		pub_rel->columns = t->columns;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1497,6 +1661,8 @@ OpenTableList(List *tables)
 				pub_rel->relation = rel;
 				/* child inherits WHERE clause from parent */
 				pub_rel->whereClause = t->whereClause;
+				/* child inherits column list from parent */
+				pub_rel->columns = t->columns;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1610,6 +1776,11 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 		Relation	rel = pubrel->relation;
 		Oid			relid = RelationGetRelid(rel);
 
+		if (pubrel->columns)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("column list must not be specified in ALTER PUBLICATION ... DROP"));
+
 		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
 							   ObjectIdGetDatum(relid),
 							   ObjectIdGetDatum(pubid));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3e83f375b55..5546d3d4387 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -625,6 +625,8 @@ static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
 static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
+static void check_replica_identity(List *relations, char relreplident,
+								   Relation rel, Bitmapset *cols);

 /* ----------------------------------------------------------------
@@ -8365,6 +8367,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 				 bool missing_ok, LOCKMODE lockmode,
 				 ObjectAddresses *addrs)
 {
+	Oid			relid = RelationGetRelid(rel);
 	HeapTuple	tuple;
 	Form_pg_attribute targetatt;
 	AttrNumber	attnum;
@@ -8384,7 +8387,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 	/*
 	 * get the number of the attribute
 	 */
-	tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+	tuple = SearchSysCacheAttName(relid, colName);
 	if (!HeapTupleIsValid(tuple))
 	{
 		if (!missing_ok)
@@ -8438,13 +8441,42 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	/*
 
 	if (children)
 	{
@@ -8532,7 +8564,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,

@@ -15864,7 +15896,28 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	Oid			indexOid;
 	Relation	indexRel;
 	int			key;
+	Bitmapset  *indexed_cols = NULL;
+	List	   *ancestors = NIL;
+
+	/*
+	 * Check that the new partition is compatible with all publications the
+	 * parent relation (to which we're attaching) is part of. Each ancestor
+	 * may be added to multiple publications - we need to check all of them,
+	 * not just the top-most, because the top-most one might be removed later.
+	 */
+	ancestors = get_partition_ancestors(RelationGetRelid(rel));
+
+	/*
+	 * Include the rel itself too, because it may already have publications.
+	 */
+	ancestors = lappend_oid(ancestors, RelationGetRelid(rel));
 
+	/*
+	 * Check that the replica identity and column filters are compatible.
+	 */
+	check_replica_identity(ancestors, stmt->identity_type, rel, NULL);
+
+	/* cross-check the column lists and replica identity (if defined) */
 	if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT)
 	{
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
@@ -15961,8 +16014,17 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 					 errmsg("index \"%s\" cannot be used as replica identity because column \"%s\" is nullable",
 							RelationGetRelationName(indexRel),
 							NameStr(attr->attname))));
+
+		/*
+		 * Collect columns used, in case we have any publications that we need
+		 * to vet. Offset by FirstLowInvalidHeapAttributeNumber, because that's
+		 * what check_replica_identity expects.
+		 */
+		indexed_cols = bms_add_member(indexed_cols, attno - FirstLowInvalidHeapAttributeNumber);
 	}
 
+	check_replica_identity(ancestors, REPLICA_IDENTITY_INDEX, rel, indexed_cols);
+
 	/* This index is suitable for use as a replica identity. Mark it. */
 	relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true);
 
@@ -17623,6 +17685,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 	Oid			defaultPartOid;
 	List	   *partBoundConstraint;
 	ParseState *pstate = make_parsestate(NULL);
+	List	   *ancestors = NIL;
 
 	pstate->p_sourcetext = context->queryString;
 
@@ -17784,6 +17847,27 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 						trigger_name, RelationGetRelationName(attachrel)),
 				 errdetail("ROW triggers with transition tables are not supported on partitions")));
 
+	/*
+	 * Check that the new partition is compatible with all publications the
+	 * parent relation (to which we're attaching) is part of. Each ancestor
+	 * may be added to multiple publications - we need to check all of them,
+	 * not just the top-most, because the top-most one might be removed later.
+	 */
+	ancestors = get_partition_ancestors(RelationGetRelid(rel));
+
+	/*
+	 * Include the rel to which we attach the new partition, so that we have
+	 * all ancestors of the attachrel.
+	 */
+	ancestors = lappend_oid(ancestors, RelationGetRelid(rel));
+
+	/*
+	 * Check the replica identity of the new partition is compatible with
+	 * all publications.
+	 */
+	check_replica_identity(ancestors, attachrel->rd_rel->relreplident,
+						   attachrel, NULL);
+
 	/*
 	 * Check that the new partition's bound is valid and does not overlap any
 	 * of existing partitions of the parent - note that it does not return on
@@ -19239,3 +19323,122 @@ GetAttributeCompression(Oid atttypid, char *compression)
 
 	return cmethod;
 }
+
+/*
+ * - relations - list of OIDs for relations for which we need to inspect the
+ * publications (this includes ancestors and possibly the relation itself)
+ *
+ * - rel - the relation for which we verify the replica identity
+ *
+ * XXX This has a bit of a problem, becase for ALTER TABLE ... REPLICA
+ * IDENTITY it's called before the relation has it assigned. So we get the
+ * relreplident and indexed columns separately. Maybe we could just call
+ * it a bit later?
+ */
+static void
+check_replica_identity(List *relations, char relreplident, Relation rel,
+					   Bitmapset *cols)
+{
+	ListCell   *lc;
+
+	/*
+	 * Check that the new partition is compatible with all publications the
+	 * ancestors are members of. Each ancestor may be added to multiple
+	 * publications - we need to check all of them, not just the top-most,
+	 * because the top-most one might be removed later.
+	 */
+	foreach(lc, relations)
+	{
+		ListCell   *lc2;
+		Oid			ancestor = lfirst_oid(lc);
+
+		/* only publications with a column filter */
+		List	   *pubids = GetRelationColumnPartialPublications(ancestor);
+
+		/* no publications with column filter for this parent */
+		if (!pubids)
+			continue;
+
+		/* With REPLICA IDENTITY FULL no column filter is allowed. */
+		if (relreplident == REPLICA_IDENTITY_FULL)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("invalid column list for publishing relation \"%s\"",
+						   RelationGetRelationName(rel)),	/* XXX maybe report the ancestor? */
+					errdetail("Cannot specify column list on relations with REPLICA IDENTITY FULL."));
+
+		/* check the column filter vs. replica identity for each partition */
+		foreach (lc2, pubids)
+		{
+			ListCell   *lc3;
+			Oid			pubid = lfirst_oid(lc2);
+			List	   *columns = GetRelationColumnListInPublication(ancestor, pubid);
+			Bitmapset  *colset = NULL;
+			Bitmapset  *idattrs;
+			Publication *pub;
+
+			pub = GetPublication(pubid);
+
+			/*
+			 * When not replicating UPDATE/DELETE, we're done. Otherwise check the
+			 * whole replica identity is included in the column filter.
+			 */
+			if (!pub->pubactions.pubupdate && !pub->pubactions.pubdelete)
+				continue;
+
+			/* Get replica identity attributes. */
+			if ((relreplident == REPLICA_IDENTITY_INDEX) && cols)
+				idattrs = cols;
+			else
+				idattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY);
+
+			/*
+			 * This can happen, in which case the replica identity is inherited
+			 * from the ancestor, which means it's OK for the publication.
+			 */
+			if (!idattrs)
+				continue;
+
+			/*
+			 * Build bitmap from the filter columns, for easy comparison with the
+			 * replica identity bitmap. Offset by FirstLowInvalidHeapAttributeNumber
+			 * for each comparison with idattrs.
+			 *
+			 * XXX Do we need to map attnums between the partitions? For now
+			 * just lookup name in the ancestor (per the column list), and then
+			 * lookup the attnum in the child. But maybe that's not needed?
+			 */
+			foreach (lc3, columns)
+			{
+				AttrNumber	attnum;
+				char	   *attname;
+
+				attnum = lfirst_oid(lc3);
+				attname = get_attname(ancestor, attnum, false);
+
+				/* reverse lookup for the current relation */
+				attnum = get_attnum(RelationGetRelid(rel), attname);
+
+				colset = bms_add_member(colset, attnum - FirstLowInvalidHeapAttributeNumber);
+			}
+
+			/*
+			 * Attnums in the bitmap returned by RelationGetIndexAttrBitmap are
+			 * offset (to handle system columns the usual way), while column filter
+			 * does not use offset, so we can't do bms_is_subset(). Instead, we have
+			 * to loop over the idattrs and check all of them are in the filter.
+			 */
+			if (!bms_is_subset(idattrs, colset))
+			{
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+						errmsg("invalid column list for publishing relation \"%s\"",
+							   RelationGetRelationName(rel)),	/* XXX maybe report the ancestor? */
+						errdetail("All columns in REPLICA IDENTITY must be present in the column list."));
+			}
+
+			bms_free(colset);
+			bms_free(idattrs);
+		}
+	}
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index d4f8455a2bd..a504437873f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4850,6 +4850,7 @@ _copyPublicationTable(const PublicationTable *from)
 
 	COPY_NODE_FIELD(relation);
 	COPY_NODE_FIELD(whereClause);
+	COPY_NODE_FIELD(columns);
 
 	return newnode;
 }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f1002afe7a0..4fc16ce04e3 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2322,6 +2322,7 @@ _equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
 {
 	COMPARE_NODE_FIELD(relation);
 	COMPARE_NODE_FIELD(whereClause);
+	COMPARE_NODE_FIELD(columns);
 
 	return true;
 }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a03b33b53bd..25c9b29afdd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9751,13 +9751,14 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr OptWhereClause
+			TABLE relation_expr opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -9772,11 +9773,12 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @5;
 				}
-			| ColId OptWhereClause
+			| ColId opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
-					if ($2)
+					/* FIXME this is a bit cumbersome */
+					if ($2 || $3)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -9786,7 +9788,8 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->whereClause = $2;
+						$$->pubtable->columns = $2;
+						$$->pubtable->whereClause = $3;
 					}
 					else
 					{
@@ -9794,23 +9797,25 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection OptWhereClause
+			| ColId indirection opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr OptWhereClause
+			| extended_relation_expr opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->whereClause = $2;
+					$$->pubtable->columns = $2;
+					$$->pubtable->whereClause = $3;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -9836,6 +9841,10 @@ pub_obj_list: 	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name SET COLUMNS table_name (column[, ...])
+ *
+ * ALTER PUBLICATION name SET COLUMNS table_name ALL
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -9869,6 +9878,32 @@ AlterPublicationStmt:
 					n->action = AP_SetObjects;
 					$$ = (Node *)n;
 				}
+			| ALTER PUBLICATION name ALTER TABLE relation_expr SET COLUMNS '(' columnList ')'
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					PublicationObjSpec *obj = makeNode(PublicationObjSpec);
+					obj->pubobjtype = PUBLICATIONOBJ_TABLE;
+					obj->pubtable = makeNode(PublicationTable);
+					obj->pubtable->relation = $6;
+					obj->pubtable->columns = $10;
+					n->pubname = $3;
+					n->pubobjects = list_make1(obj);
+					n->action = AP_SetColumns;
+					$$ = (Node *) n;
+				}
+			| ALTER PUBLICATION name ALTER TABLE relation_expr SET COLUMNS ALL
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					PublicationObjSpec *obj = makeNode(PublicationObjSpec);
+					obj->pubobjtype = PUBLICATIONOBJ_TABLE;
+					obj->pubtable = makeNode(PublicationTable);
+					obj->pubtable->relation = $6;
+					obj->pubtable->columns = NIL;
+					n->pubname = $3;
+					n->pubobjects = list_make1(obj);
+					n->action = AP_SetColumns;
+					$$ = (Node *) n;
+				}
 			| ALTER PUBLICATION name DROP pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
@@ -17488,6 +17523,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("WHERE clause not allowed for schema"),
 						parser_errposition(pubobj->location));
 
+			/* Column filter is not allowed on a schema object */
+			if (pubobj->pubtable && pubobj->pubtable->columns)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("column specification not allowed for schema"),
+						parser_errposition(pubobj->location));
+
 			/*
 			 * We can distinguish between the different type of schema
 			 * objects based on whether name and pubtable is set.
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index c9b0eeefd7e..ccca326ad6a 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -29,10 +29,11 @@
 #define TRUNCATE_CASCADE		(1<<0)
 #define TRUNCATE_RESTART_SEQS	(1<<1)
 
 								   TupleTableSlot *slot,
 
@@ -398,7 +399,7 @@ logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn)
  */
 void
 logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
-						TupleTableSlot *newslot, bool binary)
+						TupleTableSlot *newslot, bool binary, Bitmapset *columns)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_INSERT);
 
@@ -410,7 +411,7 @@ logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
 	pq_sendint32(out, RelationGetRelid(rel));
 
 	pq_sendbyte(out, 'N');		/* new tuple follows */
-	logicalrep_write_tuple(out, rel, newslot, binary);
+	logicalrep_write_tuple(out, rel, newslot, binary, columns);
 }
 
 /*
@@ -443,7 +444,7 @@ logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup)
 void
 logicalrep_write_update(StringInfo out, TransactionId xid, Relation rel,
 						TupleTableSlot *oldslot, TupleTableSlot *newslot,
-						bool binary)
+						bool binary, Bitmapset *columns)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_UPDATE);
 
@@ -464,11 +465,11 @@ logicalrep_write_update(StringInfo out, TransactionId xid, Relation rel,
 			pq_sendbyte(out, 'O');	/* old tuple follows */
 		else
 			pq_sendbyte(out, 'K');	/* old key follows */
-		logicalrep_write_tuple(out, rel, oldslot, binary);
+		logicalrep_write_tuple(out, rel, oldslot, binary, columns);
 	}
 
 	pq_sendbyte(out, 'N');		/* new tuple follows */
-	logicalrep_write_tuple(out, rel, newslot, binary);
+	logicalrep_write_tuple(out, rel, newslot, binary, columns);
 }
 
 /*
@@ -537,7 +538,7 @@ logicalrep_write_delete(StringInfo out, TransactionId xid, Relation rel,
 	else
 		pq_sendbyte(out, 'K');	/* old key follows */
 
-	logicalrep_write_tuple(out, rel, oldslot, binary);
+	logicalrep_write_tuple(out, rel, oldslot, binary, NULL);
 }
 
 /*
@@ -652,7 +653,8 @@ logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
  * Write relation description to the output stream.
  */
 void
-logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
+logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel,
+					 Bitmapset *columns)
 {
 	char	   *relname;
 
@@ -674,7 +676,7 @@ logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
 	pq_sendbyte(out, rel->rd_rel->relreplident);

 /*
@@ -751,7 +753,7 @@ logicalrep_read_typ(StringInfo in, LogicalRepTyp *ltyp)
  */
 static void
 logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
-					   bool binary)
+					   bool binary, Bitmapset *columns)
 {
 	TupleDesc	desc;
 	Datum	   *values;
@@ -763,7 +765,13 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 
@@ -783,6 +791,14 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 
@@ -904,7 +920,7 @@ logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple)
@@ -917,8 +933,15 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
@@ -937,6 +960,10 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 
index 1659964571c..42708dcf82e 100644
@@ -700,20 +701,22 @@ fetch_remote_table_info(char *nspname, char *relname,
 	Oid			qualRow[] = {TEXTOID};
 	ListCell   *lc;
 	bool		first;
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -743,14 +746,225 @@ fetch_remote_table_info(char *nspname, char *relname,

-	/* Now fetch columns. */
+
+	/*
+	 * Get relation's column filter expressions.
+	 *
+	 * For initial synchronization, column filter can be ignored in following
+	 * cases:
+	 *
+	 * 1) one of the subscribed publications for the table hasn't specified
+	 * any column filter
+	 *
+	 * 2) one of the subscribed publications has puballtables set to true
+	 *
+	 * 3) one of the subscribed publications is declared as ALL TABLES IN
+	 * SCHEMA that includes this relation
+	 */
+	if (walrcv_server_version(LogRepWorkerWalRcvConn) >= 150000)
+	{
+		WalRcvExecResult *pubres;
+		TupleTableSlot *slot;
+		Oid			attrsRow[] = {INT2OID};
+		Oid			tmpRow[] = {INT4OID};
+		StringInfoData publications;
+		bool		first = true;
+		bool		all_columns = false;
+
+		initStringInfo(&publications);
+		foreach(lc, MySubscription->publications)
+		{
+			if (!first)
+				appendStringInfo(&publications, ", ");
+			appendStringInfoString(&publications, quote_literal_cstr(strVal(lfirst(lc))));
+			first = false;
+		}
+
+		/*
+		 * First, check if any of the publications FOR ALL TABLES? If yes, we
+		 * should not use any column filter. It's enough to find a single such
+		 * publication.
+		 *
+		 * XXX Maybe we could combine all three steps into a single query, but
+		 * this seems cleaner / easier to understand.
+		 *
+		 * XXX Does this need any handling of partitions / publish_via_part_root?
+		 */
+		resetStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT 1\n"
+						 "  FROM pg_publication p\n"
+						 " WHERE p.pubname IN ( %s ) AND p.puballtables LIMIT 1\n",
+						 publications.data);
+
+		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+							 lengthof(tmpRow), tmpRow);
+
+		if (pubres->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not fetch publication info for table \"%s.%s\" from publisher: %s",
+							nspname, relname, pubres->err)));
+
+		slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+
+		if (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+			all_columns = true;
+
+		ExecDropSingleTupleTableSlot(slot);
+		walrcv_clear_result(pubres);
+
+		/*
+		 * If there's no FOR ALL TABLES publication, look for a FOR ALL TABLES
+		 * IN SCHEMA publication, with schema of the remote relation. The logic
+		 * is the same - such publications have no column filters.
+		 *
+		 * XXX Does this need any handling of partitions / publish_via_part_root?
+		 */
+		if (!all_columns)
+		{
+			resetStringInfo(&cmd);
+			appendStringInfo(&cmd,
+							 "SELECT 1\n"
+							 "  FROM pg_publication p\n"
+							 "       JOIN pg_publication_namespace pn ON (pn.pnpubid = p.oid)\n"
+							 "       JOIN pg_class c ON (pn.pnnspid = c.relnamespace)\n"
+							 " WHERE c.oid = %u AND p.pubname IN ( %s ) LIMIT 1",
+							 lrel->remoteid,
+							 publications.data);
+
+			pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+								 lengthof(tmpRow), tmpRow);
+
+			if (pubres->status != WALRCV_OK_TUPLES)
+				ereport(ERROR,
+						(errcode(ERRCODE_CONNECTION_FAILURE),
+						 errmsg("could not fetch publication info for table \"%s.%s\" from publisher: %s",
+								nspname, relname, pubres->err)));
+
+			slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+
+			if (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+				all_columns = true;
+
+			ExecDropSingleTupleTableSlot(slot);
+			walrcv_clear_result(pubres);
+		}
+
+		/*
+		 * If we haven't found any FOR ALL TABLES [IN SCHEMA] publications for
+		 * the table, we have to look for the column filters set for relations.
+		 * First, we check if there's a publication with no column filter for
+		 * the relation - which means all columns need to be replicated.
+		 */
+		if (!all_columns)
+		{
+			resetStringInfo(&cmd);
+			appendStringInfo(&cmd,
+							 "SELECT 1\n"
+							 "  FROM pg_catalog.pg_publication p JOIN\n"
+							 "       pg_catalog.pg_publication_rel pr ON (p.oid = pr.prpubid)\n"
+							 " WHERE p.pubname IN (%s) AND pr.prattrs IS NULL AND ",
+							 publications.data);
+
+			/*
+			 * For non-partitions, we simply join directly to the catalog. For
+			 * partitions, we need to check all the ancestors, because maybe the
+			 * root was not added to a publication but one of the intermediate
+			 * partitions was.
+			 */
+			if (!am_partition)
+				appendStringInfo(&cmd, "prrelid = %u", lrel->remoteid);
+			else
+				appendStringInfo(&cmd,
+								 "prrelid IN (SELECT relid\n"
+								 "    FROM pg_catalog.pg_partition_tree(pg_catalog.pg_partition_root(%u)))",
+								 lrel->remoteid);
+
+			pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+								 lengthof(tmpRow), tmpRow);
+
+			if (pubres->status != WALRCV_OK_TUPLES)
+				ereport(ERROR,
+						(errcode(ERRCODE_CONNECTION_FAILURE),
+						 errmsg("could not fetch attribute info for table \"%s.%s\" from publisher: %s",
+								nspname, relname, pubres->err)));
+
+			slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+
+			if (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+				all_columns = true;
+
+			ExecDropSingleTupleTableSlot(slot);
+			walrcv_clear_result(pubres);
+		}
+
+		/*
+		 * All that 
+		 */
+		if (!all_columns)
+		{
+			resetStringInfo(&cmd);
+			appendStringInfo(&cmd,
+							 "SELECT unnest(pr.prattrs)\n"
+							 "  FROM pg_catalog.pg_publication p JOIN\n"
+							 "       pg_catalog.pg_publication_rel pr ON (p.oid = pr.prpubid)\n"
+							 " WHERE p.pubname IN (%s) AND pr.prattrs IS NOT NULL AND ",
+							 publications.data);
+
+			/*
+			 * For non-partitions, we simply join directly to the catalog. For
+			 * partitions, we need to check all the ancestors, because maybe the
+			 * root was not added to a publication but one of the intermediate
+			 * partitions was.
+			 */
+			if (!am_partition)
+				appendStringInfo(&cmd, "prrelid = %u", lrel->remoteid);
+			else
+				appendStringInfo(&cmd,
+								 "prrelid IN (SELECT relid\n"
+								 "    FROM pg_catalog.pg_partition_tree(pg_catalog.pg_partition_root(%u)))",
+								 lrel->remoteid);
+
+			pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+								 lengthof(attrsRow), attrsRow);
+
+			if (pubres->status != WALRCV_OK_TUPLES)
+				ereport(ERROR,
+						(errcode(ERRCODE_CONNECTION_FAILURE),
+						 errmsg("could not fetch attribute info for table \"%s.%s\" from publisher: %s",
+								nspname, relname, pubres->err)));
+
+			slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+			while (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+			{
+				AttrNumber	attnum;
+
+				attnum = DatumGetInt16(slot_getattr(slot, 1, &isnull));
+				Assert(!isnull);
+
+				included_cols = bms_add_member(included_cols, attnum);
+			}
+			ExecDropSingleTupleTableSlot(slot);
+			walrcv_clear_result(pubres);
+		}
+
+		pfree(publications.data);
+	}
+
+	/*
+	 * Now fetch column names and types.
+	 */
 	resetStringInfo(&cmd);
 	appendStringInfo(&cmd,
-					 "SELECT a.attname,"
+					 "SELECT a.attnum,"
+					 "       a.attname,"
 					 "       a.atttypid,"
 					 "       a.attnum = ANY(i.indkey)"
 					 "  FROM pg_catalog.pg_attribute a"
@@ -778,16 +992,34 @@ fetch_remote_table_info(char *nspname, char *relname,
 	lrel->atttyps = palloc0(MaxTupleAttributeNumber * sizeof(Oid));
 	lrel->attkeys = NULL;
 
+	/*
+			ExecClearTuple(slot);
 
 		/* Should never happen. */
@@ -821,6 +1053,9 @@ fetch_remote_table_info(char *nspname, char *relname,
 	 *
 	 * 3) one of the subscribed publications is declared as ALL TABLES IN
 	 * SCHEMA that includes this relation
+	 *
+	 * XXX Does this actually handle puballtables and schema publications
+	 * correctly?
 	 */
 	if (walrcv_server_version(LogRepWorkerWalRcvConn) >= 150000)
 	{
@@ -930,8 +1165,24 @@ copy_table(Relation rel)
 
 	/* Regular table with no row filter */
 	if (lrel.relkind == RELKIND_RELATION && qual == NIL)
-		appendStringInfo(&cmd, "COPY %s TO STDOUT",
+	{
+		appendStringInfo(&cmd, "COPY %s (",
 						 quote_qualified_identifier(lrel.nspname, lrel.relname));
+
+		/*
+		 * XXX Do we need to list the columns in all cases? Maybe we're replicating
+		 * all columns?
+		 */
+		for (int i = 0; i < lrel.natts; i++)
+		{
+			if (i > 0)
+				appendStringInfoString(&cmd, ", ");
+
+			appendStringInfoString(&cmd, quote_identifier(lrel.attnames[i]));
+		}
+
+		appendStringInfo(&cmd, ") TO STDOUT");
+	}
 	else
 	{
 		/*
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index ea57a0477f0..16239cc686d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -29,6 +29,7 @@
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
@@ -85,7 +86,8 @@ static List *LoadPublications(List *pubnames);
@@ -164,6 +166,13 @@ typedef struct RelationSyncEntry
 	AttrMap    *attrmap;
 
 /* Map used to remember which relation schemas we sent. */
@@ -603,11 +612,11 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 	{
 		Relation	ancestor = RelationIdGetRelation(relentry->publish_as_relid);

 
 	if (in_streaming)
 		set_schema_sent_in_streamed_txn(relentry, topxid);
@@ -620,7 +629,8 @@ maybe_send_schema(LogicalDecodingContext *ctx,
  */
 static void
 send_relation_and_attrs(Relation relation, TransactionId xid,
-						LogicalDecodingContext *ctx)
+						LogicalDecodingContext *ctx,
+						Bitmapset *columns)
 {
 	TupleDesc	desc = RelationGetDescr(relation);
 	int			i;
@@ -643,13 +653,17 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		if (att->atttypid < FirstGenbkiObjectId)
 			continue;

 
@@ -1224,7 +1238,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 			OutputPluginPrepareWrite(ctx, true);
 			logicalrep_write_insert(ctx->out, xid, targetrel, new_slot,
-									data->binary);
+									data->binary, relentry->columns);
 			OutputPluginWrite(ctx, true);
 			break;
 		case REORDER_BUFFER_CHANGE_UPDATE:
@@ -1278,11 +1292,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 			{
 				case REORDER_BUFFER_CHANGE_INSERT:
 					logicalrep_write_insert(ctx->out, xid, targetrel,
-											new_slot, data->binary);
+											new_slot, data->binary,
+											relentry->columns);
 					break;
 				case REORDER_BUFFER_CHANGE_UPDATE:
 					logicalrep_write_update(ctx->out, xid, targetrel,
-											old_slot, new_slot, data->binary);
+											old_slot, new_slot, data->binary,
+											relentry->columns);
 					break;
 				case REORDER_BUFFER_CHANGE_DELETE:
 					logicalrep_write_delete(ctx->out, xid, targetrel,
@@ -1731,6 +1747,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		memset(entry->exprstate, 0, sizeof(entry->exprstate));
 		entry->cache_expr_cxt = NULL;
 		entry->publish_as_relid = InvalidOid;
+		entry->columns = NULL;
 		entry->attrmap = NULL;
 	}
 
@@ -1775,6 +1792,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->schema_sent = false;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
+		bms_free(entry->columns);
+		entry->columns = NULL;
 		entry->pubactions.pubinsert = false;
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
@@ -1807,13 +1826,16 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		/*
 
 			if (pub->alltables)
 			{
@@ -1824,8 +1846,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 			if (!publish)
 			{
-				bool		ancestor_published = false;
-
 				/*
 				 * For a partition, check if any of the ancestors are
 				 * published.  If so, note down the topmost ancestor that is
@@ -1855,6 +1875,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			}
 
 			/*
+			 * If the relation is to be published, determine actions to
+			 * publish, and list of columns, if appropriate.
+			 *
 			 * Don't publish changes for partitioned tables, because
 			 * publishing those of its partitions suffices, unless partition
 			 * changes won't be published due to pubviaroot being set.
@@ -1867,6 +1890,80 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				entry->pubactions.pubdelete |= pub->pubactions.pubdelete;
 				entry->pubactions.pubtruncate |= pub->pubactions.pubtruncate;
 
+				/*
+				 * This might be FOR ALL TABLES or FOR ALL TABLES IN SCHEMA
+				 * publication, in which case there are no column lists, and
+				 * we treat that as all_columns=true.
+				 */
+				if (pub->alltables ||
+					list_member_oid(schemaPubids, pub->oid))
+				{
+					all_columns = true;
+					bms_free(entry->columns);
+					entry->columns = NULL;
+				}
+
+				/*
+				 * Obtain columns published by this publication, and add them
+				 * to the list for this rel.  Note that if at least one
+				 * publication has an empty column list, that means to publish
+				 * everything; so if we saw a publication that includes all
+				 * columns, skip this.
+				 *
+				 * FIXME This fails to consider column filters defined in
+				 * FOR ALL TABLES and FOR ALL TABLES IN SCHEMA publications.
+				 * We need to check those too.
+				 */
+				if (!all_columns)
+				{
+					HeapTuple	pub_rel_tuple;
+
+					pub_rel_tuple = SearchSysCache2(PUBLICATIONRELMAP,
+													ObjectIdGetDatum(publish_as_relid),
+													ObjectIdGetDatum(pub->oid));
+
+					if (HeapTupleIsValid(pub_rel_tuple))
+					{
+						Datum		pub_rel_cols;
+						bool		isnull;
+
+						pub_rel_cols = SysCacheGetAttr(PUBLICATIONRELMAP,
+													   pub_rel_tuple,
+													   Anum_pg_publication_rel_prattrs,
+													   &isnull);
+						if (isnull)
+						{
+							/*
+							 * If we see a publication with no column filter, it
+							 * means we need to publish all columns, so reset the
+							 * list and ignore further ones.
+							 */
+							all_columns = true;
+							bms_free(entry->columns);
+							entry->columns = NULL;
+						}
+						else
+						{
+							ArrayType  *arr;
+							int			nelems;
+							int16	   *elems;
+
+							arr = DatumGetArrayTypeP(pub_rel_cols);
+							nelems = ARR_DIMS(arr)[0];
+							elems = (int16 *) ARR_DATA_PTR(arr);
+
+							/* XXX is there a danger of memory leak here? beware */
+							oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+							for (int i = 0; i < nelems; i++)
+								entry->columns = bms_add_member(entry->columns,
+																elems[i]);
+							MemoryContextSwitchTo(oldctx);
+						}
+
+						ReleaseSysCache(pub_rel_tuple);
+					}
+				}
+
 				rel_publications = lappend(rel_publications, pub);
 			}
 		}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e69dcf8a484..f208c7a6c59 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4075,6 +4075,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prpubid;
 	int			i_prrelid;
 	int			i_prrelqual;
+	int			i_prattrs;
 	int			i,
 				j,
 				ntups;
@@ -4088,12 +4089,20 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	if (fout->remoteVersion >= 150000)
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
-							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual "
-							 "FROM pg_catalog.pg_publication_rel");
+							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
+							 "(CASE\n"
+							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
+							 "    (SELECT array_agg(attname)\n"
+							 "       FROM\n"
+							 "         pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+							 "         pg_catalog.pg_attribute\n"
+							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+							 "  ELSE NULL END) prattrs "
+							 "FROM pg_catalog.pg_publication_rel pr");
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
-							 "NULL AS prrelqual "
+							 "NULL AS prrelqual, NULL AS prattrs "
 							 "FROM pg_catalog.pg_publication_rel");
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
@@ -4104,6 +4113,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prpubid = PQfnumber(res, "prpubid");
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
+	i_prattrs = PQfnumber(res, "prattrs");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4149,6 +4159,28 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));

@@ -4223,10 +4255,13 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)

-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY ",
 					  fmtId(pubinfo->dobj.name));
-	appendPQExpBuffer(query, " %s",
-					  fmtQualifiedDumpable(tbinfo));
+	appendPQExpBufferStr(query, fmtQualifiedDumpable(tbinfo));
+
+	if (pubrinfo->pubrattrs)
+		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+
 	if (pubrinfo->pubrelqual)
 	{
 		/*
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 997a3b60719..680b07dcd52 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -632,6 +632,7 @@ typedef struct _PublicationRelInfo
 	PublicationInfo *publication;
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
+	char	   *pubrattrs;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c3bcef8c0ec..1fedcdb442f 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2418,6 +2418,28 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'ALTER PUBLICATION pub1 ADD TABLE test_sixth_table (col3, col2)' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub1 ADD TABLE dump_test.test_sixth_table (col3, col2);',
+		regexp => qr/^
+			\QALTER PUBLICATION pub1 ADD TABLE ONLY dump_test.test_sixth_table (col2, col3);\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'ALTER PUBLICATION pub1 ADD TABLE test_seventh_table (col3, col2) WHERE (col1 = 1)' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub1 ADD TABLE dump_test.test_seventh_table (col3, col2) WHERE (col1 = 1);',
+		regexp => qr/^
+			\QALTER PUBLICATION pub1 ADD TABLE ONLY dump_test.test_seventh_table (col2, col3) WHERE ((col1 = 1));\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'ALTER PUBLICATION pub3 ADD ALL TABLES IN SCHEMA dump_test' => {
 		create_order => 51,
 		create_sql =>
@@ -2743,6 +2765,44 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'CREATE TABLE test_sixth_table' => {
+		create_order => 6,
+		create_sql   => 'CREATE TABLE dump_test.test_sixth_table (
+						   col1 int,
+						   col2 text,
+						   col3 bytea
+					   );',
+		regexp => qr/^
+			\QCREATE TABLE dump_test.test_sixth_table (\E
+			\n\s+\Qcol1 integer,\E
+			\n\s+\Qcol2 text,\E
+			\n\s+\Qcol3 bytea\E
+			\n\);
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE TABLE test_seventh_table' => {
+		create_order => 6,
+		create_sql   => 'CREATE TABLE dump_test.test_seventh_table (
+						   col1 int,
+						   col2 text,
+						   col3 bytea
+					   );',
+		regexp => qr/^
+			\QCREATE TABLE dump_test.test_seventh_table (\E
+			\n\s+\Qcol1 integer,\E
+			\n\s+\Qcol2 text,\E
+			\n\s+\Qcol3 bytea\E
+			\n\);
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE TABLE test_table_identity' => {
 		create_order => 3,
 		create_sql   => 'CREATE TABLE dump_test.test_table_identity (
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e3382933d98..fb18cb82d9f 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2880,6 +2880,7 @@ describeOneTableDetails(const char *schemaname,
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "		JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -2887,6 +2888,12 @@ describeOneTableDetails(const char *schemaname,
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, pg_get_expr(pr.prqual, c.oid)\n"
+								  "		, (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "			(SELECT string_agg(attname, ', ')\n"
+								  "			  FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "				   pg_catalog.pg_attribute\n"
+								  "			 WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "		   ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
@@ -2894,6 +2901,7 @@ describeOneTableDetails(const char *schemaname,
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
 								  "ORDER BY 1;",
@@ -2904,12 +2912,14 @@ describeOneTableDetails(const char *schemaname,
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "WHERE pr.prrelid = '%s'\n"
 								  "UNION ALL\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
 								  "ORDER BY 1;",
@@ -2931,6 +2941,11 @@ describeOneTableDetails(const char *schemaname,
 				printfPQExpBuffer(&buf, "    \"%s\"",
 								  PQgetvalue(result, i, 0));
 
+				/* column filter (if any) */
+				if (!PQgetisnull(result, i, 2))
+					appendPQExpBuffer(&buf, " (%s)",
+									  PQgetvalue(result, i, 2));
+
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
 					appendPQExpBuffer(&buf, " WHERE %s",
@@ -5867,7 +5882,7 @@ listPublications(const char *pattern)
  */
 static bool
 addFooterToPublicationDesc(PQExpBuffer buf, char *footermsg,
-						   bool singlecol, printTableContent *cont)
+						   bool as_schema, printTableContent *cont)
 {
 	PGresult   *res;
 	int			count = 0;
@@ -5884,15 +5899,19 @@ addFooterToPublicationDesc(PQExpBuffer buf, char *footermsg,
 
 	for (i = 0; i < count; i++)
 	{
-		if (!singlecol)
+		if (as_schema)
+			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
+		else
 		{
 			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
 							  PQgetvalue(res, i, 1));
+
+			if (!PQgetisnull(res, i, 3))
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+
 			if (!PQgetisnull(res, i, 2))
 				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
 		}
-		else
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
 
 		printTableAddFooter(cont, buf->data);
 	}
@@ -6021,11 +6040,22 @@ describePublications(const char *pattern)
 			printfPQExpBuffer(&buf,
 							  "SELECT n.nspname, c.relname");
 			if (pset.sversion >= 150000)
+			{
 				appendPQExpBufferStr(&buf,
 									 ", pg_get_expr(pr.prqual, c.oid)");
+				appendPQExpBufferStr(&buf,
+									 ", (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+									 "     pg_catalog.array_to_string("
+									 "      ARRAY(SELECT attname\n"
+									 "              FROM\n"
+									 "                pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+									 "                pg_catalog.pg_attribute\n"
+									 "        WHERE attrelid = c.oid AND attnum = prattrs[s]), ', ')\n"
+									 "       ELSE NULL END)");
+			}
 			else
 				appendPQExpBufferStr(&buf,
-									 ", NULL");
+									 ", NULL, NULL");
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index ba72e62e614..0190b91c091 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -100,6 +100,7 @@ typedef struct PublicationRelInfo
 {
 	Relation	relation;
 	Node	   *whereClause;
+	List	   *columns;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
@@ -123,8 +124,11 @@ typedef enum PublicationPartOpt
 } PublicationPartOpt;
 
@@ -142,6 +146,8 @@ extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri
 
 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 0dd0f425db9..4feb581899e 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -34,6 +34,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
+	int2vector	prattrs;		/* columns to replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 1617702d9d6..1375a173e3b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3652,6 +3652,7 @@ typedef struct PublicationTable
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
+	List	   *columns;		/* List of columns in a publication table */
 } PublicationTable;
 
 /*
@@ -3688,7 +3689,8 @@ typedef enum AlterPublicationAction
 {
 
 typedef struct AlterPublicationStmt
diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h
index 4d2c881644a..a771ab8ff33 100644
--- a/src/include/replication/logicalproto.h
+++ b/src/include/replication/logicalproto.h
@@ -209,12 +209,12 @@ extern char *logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn);
 extern void logicalrep_write_insert(StringInfo out, TransactionId xid,
 									Relation rel,
 									TupleTableSlot *newslot,
-									bool binary);
+									bool binary, Bitmapset *columns);
 extern LogicalRepRelId logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup);
 extern void logicalrep_write_update(StringInfo out, TransactionId xid,
 									Relation rel,
 									TupleTableSlot *oldslot,
-									TupleTableSlot *newslot, bool binary);
+									TupleTableSlot *newslot, bool binary, Bitmapset *columns);
 extern LogicalRepRelId logicalrep_read_update(StringInfo in,
 											  bool *has_oldtuple, LogicalRepTupleData *oldtup,
 											  LogicalRepTupleData *newtup);
@@ -231,7 +231,7 @@ extern List *logicalrep_read_truncate(StringInfo in,
 extern void logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
 									 bool transactional, const char *prefix, Size sz, const char *message);
 extern void logicalrep_write_rel(StringInfo out, TransactionId xid,
-								 Relation rel);
+								 Relation rel, Bitmapset *columns);
 extern LogicalRepRelation *logicalrep_read_rel(StringInfo in);
 extern void logicalrep_write_typ(StringInfo out, TransactionId xid,
 								 Oid typoid);
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 227ce759486..09e86e3049e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -657,6 +657,209 @@ DROP TABLE rf_tbl_abcd_pk;
 DROP TABLE rf_tbl_abcd_nopk;
 DROP TABLE rf_tbl_abcd_part_pk;
 -- ======================================================
+-- test for column filters
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
+CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
+RESET client_min_messages;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
+	d int generated always as (a + length(b)) stored);
+-- error: column "x" does not exist
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
+ERROR:  column "x" of relation "testpub_tbl5" does not exist
+-- error: replica identity "a" not included in the column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);
+ERROR:  invalid column list for publishing relation "testpub_tbl5"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+-- error: generated column "d" can't be in list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
+ERROR:  cannot reference generated column "d" in publication column list
+-- error: system attributes "ctid" not allowed in column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, ctid);
+ERROR:  cannot reference system column "ctid" in publication column list
+-- ok
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
+ERROR:  cannot drop column "c" because it is part of publication "testpub_fortable"
+HINT:  Specify CASCADE or use ALTER PUBLICATION to remove the column from the publication.
+-- ok: for insert-only publication, the filter is arbitrary
+ALTER PUBLICATION testpub_fortable_insert ADD TABLE testpub_tbl5 (b, c);
+/* not all replica identities are good enough */
+CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
+ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key; -- nope
+ERROR:  invalid column list for publishing relation "testpub_tbl5"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+-- error: change the replica identity to "b", and then filter (a, c) fails
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+ERROR:  invalid column list for publishing relation "testpub_tbl5"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+/* But if upd/del are not published, it works OK */
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
+RESET client_min_messages;
+ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
+\dRp+ testpub_table_ins
+                               Publication testpub_table_ins
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | t         | f
+Tables:
+    "public.testpub_tbl5" (a)
+
+-- with REPLICA IDENTITY FULL, column filters are not allowed
+CREATE TABLE testpub_tbl6 (a int, b text, c text);
+ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);
+ERROR:  invalid column list for publishing relation "testpub_tbl6"
+DETAIL:  Cannot specify column list on relations with REPLICA IDENTITY FULL.
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
+ALTER PUBLICATION testpub_fortable ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);
+ERROR:  invalid column list for publishing relation "testpub_tbl6"
+DETAIL:  Cannot specify column list on relations with REPLICA IDENTITY FULL.
+-- make sure changing the column filter is updated in SET TABLE
+CREATE TABLE testpub_tbl7 (a int primary key, b text, c text);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+                                Table "public.testpub_tbl7"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           | not null |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+ c      | text    |           |          |         | extended |              | 
+Indexes:
+    "testpub_tbl7_pkey" PRIMARY KEY, btree (a)
+Publications:
+    "testpub_fortable" (a, b)
+
+-- ok: we'll skip this table
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+                                Table "public.testpub_tbl7"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           | not null |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+ c      | text    |           |          |         | extended |              | 
+Indexes:
+    "testpub_tbl7_pkey" PRIMARY KEY, btree (a)
+Publications:
+    "testpub_fortable" (a, b)
+
+-- ok: update the column filter
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c);
+\d+ testpub_tbl7
+                                Table "public.testpub_tbl7"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           | not null |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+ c      | text    |           |          |         | extended |              | 
+Indexes:
+    "testpub_tbl7_pkey" PRIMARY KEY, btree (a)
+Publications:
+    "testpub_fortable" (a, c)
+
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 0);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 1);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (b);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+-- ok: column filter covers both "a" and "b"
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
+RESET client_min_messages;
+-- ok: the same thing, but try plain ADD TABLE
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- failure: column filter does not cover replica identity for the second partition
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+ERROR:  invalid column list for publishing relation "testpub_tbl8"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+-- failure: one of the partitions has REPLICA IDENTITY FULL
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+ERROR:  invalid column list for publishing relation "testpub_tbl8"
+DETAIL:  Cannot specify column list on relations with REPLICA IDENTITY FULL.
+-- add table and then try changing replica identity
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- failure: replica identity full can't be used with a column filter
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+ERROR:  invalid column list for publishing relation "testpub_tbl8_1"
+DETAIL:  Cannot specify column list on relations with REPLICA IDENTITY FULL.
+-- failure: replica identity has to be covered by the column filter
+ALTER TABLE testpub_tbl8_1 DROP CONSTRAINT testpub_tbl8_1_pkey;
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+ERROR:  invalid column list for publishing relation "testpub_tbl8_1"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+DROP TABLE testpub_tbl8;
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+-- ok: attaching first partition works, because (a) is in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_0 FOR VALUES WITH (modulus 2, remainder 0);
+-- failure: second partition has replica identity (c), which si not in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_1 FOR VALUES WITH (modulus 2, remainder 1);
+ERROR:  invalid column list for publishing relation "testpub_tbl8_1"
+DETAIL:  All columns in REPLICA IDENTITY must be present in the column list.
+-- failure: changing replica identity to FULL for partition fails, because
+-- of the column filter on the parent
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY FULL;
+ERROR:  invalid column list for publishing relation "testpub_tbl8_0"
+DETAIL:  Cannot specify column list on relations with REPLICA IDENTITY FULL.
+DROP TABLE testpub_tbl5, testpub_tbl6, testpub_tbl7, testpub_tbl8, testpub_tbl8_1;
+DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_filter;
+-- ======================================================
+-- Test combination of column and row filter
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_both_filters;
+RESET client_min_messages;
+CREATE TABLE testpub_tbl_both_filters (a int, b int, c int, PRIMARY KEY (a,c));
+ALTER TABLE testpub_tbl_both_filters REPLICA IDENTITY USING INDEX testpub_tbl_both_filters_pkey;
+ALTER PUBLICATION testpub_both_filters ADD TABLE testpub_tbl_both_filters (a,c) WHERE (c != 1);
+\dRp+ testpub_both_filters
+                              Publication testpub_both_filters
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "public.testpub_tbl_both_filters" (a, c) WHERE (c <> 1)
+
+\d+ testpub_tbl_both_filters
+                         Table "public.testpub_tbl_both_filters"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+Indexes:
+    "testpub_tbl_both_filters_pkey" PRIMARY KEY, btree (a, c) REPLICA IDENTITY
+Publications:
+    "testpub_both_filters" (a, c) WHERE (c <> 1)
+
+DROP TABLE testpub_tbl_both_filters;
+DROP PUBLICATION testpub_both_filters;
+-- ======================================================
 -- Test cache invalidation FOR ALL TABLES publication
 SET client_min_messages = 'ERROR';
 CREATE TABLE testpub_tbl4(a int);
@@ -1102,6 +1305,15 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_tes
 Tables from schemas:
     "pub_test1"
 
+-- Verify that it fails to add a schema with a column specification
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo (a, b);
+ERROR:  syntax error at or near "("
+LINE 1: ...TION testpub1_forschema ADD ALL TABLES IN SCHEMA foo (a, b);
+                                                                ^
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo, bar (a, b);
+ERROR:  column specification not allowed for schema
+LINE 1: ... testpub1_forschema ADD ALL TABLES IN SCHEMA foo, bar (a, b)...
+                                                             ^
 -- cleanup pub_test1 schema for invalidation tests
 ALTER PUBLICATION testpub2_forschema DROP ALL TABLES IN SCHEMA pub_test1;
 DROP PUBLICATION testpub3_forschema, testpub4_forschema, testpub5_forschema, testpub6_forschema, testpub_fortable;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index cd7e0182716..6cd4d1ae1b3 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -378,6 +378,142 @@ DROP TABLE rf_tbl_abcd_nopk;
 DROP TABLE rf_tbl_abcd_part_pk;
 -- ======================================================
 
+-- test for column filters
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
+CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
+RESET client_min_messages;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
+	d int generated always as (a + length(b)) stored);
+-- error: column "x" does not exist
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
+-- error: replica identity "a" not included in the column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);
+-- error: generated column "d" can't be in list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
+-- error: system attributes "ctid" not allowed in column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, ctid);
+-- ok
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
+-- ok: for insert-only publication, the filter is arbitrary
+ALTER PUBLICATION testpub_fortable_insert ADD TABLE testpub_tbl5 (b, c);
+
+/* not all replica identities are good enough */
+CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
+ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key; -- nope
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+
+-- error: change the replica identity to "b", and then filter (a, c) fails
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+
+/* But if upd/del are not published, it works OK */
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
+RESET client_min_messages;
+ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
+\dRp+ testpub_table_ins
+
+-- with REPLICA IDENTITY FULL, column filters are not allowed
+CREATE TABLE testpub_tbl6 (a int, b text, c text);
+ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
+ALTER PUBLICATION testpub_fortable ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);
+
+-- make sure changing the column filter is updated in SET TABLE
+CREATE TABLE testpub_tbl7 (a int primary key, b text, c text);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+-- ok: we'll skip this table
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+-- ok: update the column filter
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c);
+\d+ testpub_tbl7
+
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 0);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 1);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (b);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+
+-- ok: column filter covers both "a" and "b"
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
+RESET client_min_messages;
+
+-- ok: the same thing, but try plain ADD TABLE
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+
+-- failure: column filter does not cover replica identity for the second partition
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+
+-- failure: one of the partitions has REPLICA IDENTITY FULL
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+
+-- add table and then try changing replica identity
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- failure: replica identity full can't be used with a column filter
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+-- failure: replica identity has to be covered by the column filter
+ALTER TABLE testpub_tbl8_1 DROP CONSTRAINT testpub_tbl8_1_pkey;
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+
+DROP TABLE testpub_tbl8;
+
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+
+-- ok: attaching first partition works, because (a) is in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_0 FOR VALUES WITH (modulus 2, remainder 0);
+-- failure: second partition has replica identity (c), which si not in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_1 FOR VALUES WITH (modulus 2, remainder 1);
+-- failure: changing replica identity to FULL for partition fails, because
+-- of the column filter on the parent
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY FULL;
+
+DROP TABLE testpub_tbl5, testpub_tbl6, testpub_tbl7, testpub_tbl8, testpub_tbl8_1;
+DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_filter;
+-- ======================================================
+
+-- Test combination of column and row filter
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_both_filters;
+RESET client_min_messages;
+CREATE TABLE testpub_tbl_both_filters (a int, b int, c int, PRIMARY KEY (a,c));
+ALTER TABLE testpub_tbl_both_filters REPLICA IDENTITY USING INDEX testpub_tbl_both_filters_pkey;
+ALTER PUBLICATION testpub_both_filters ADD TABLE testpub_tbl_both_filters (a,c) WHERE (c != 1);
+\dRp+ testpub_both_filters
+\d+ testpub_tbl_both_filters
+
+DROP TABLE testpub_tbl_both_filters;
+DROP PUBLICATION testpub_both_filters;
+-- ======================================================
+
 -- Test cache invalidation FOR ALL TABLES publication
 SET client_min_messages = 'ERROR';
 CREATE TABLE testpub_tbl4(a int);
@@ -619,6 +755,10 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA non_existent_schem
 ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_test1;
 \dRp+ testpub1_forschema
 
diff --git a/src/test/subscription/t/029_column_list.pl b/src/test/subscription/t/029_column_list.pl
index 00000000000..ec2c8a789ad
+++ b/src/test/subscription/t/029_column_list.pl
@@ -0,0 +1,836 @@
+use Test::More tests => 26;
+sub wait_for_subscription_sync
+	my ($node) = @_;
+	# Also wait for initial table sync to finish
+	my $synced_query = "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+	$node->poll_query_until('postgres', $synced_query)
+		or die "Timed out while waiting for subscriber to synchronize data";
+	   FOR TABLE tab1 (a, "B"), tab3 ("a'", "c'"), test_part (a, b), tab4 (a, b, d)
+	  WITH (publish_via_partition_root = 'true');
+wait_for_subscription_sync($node_subscriber);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub2, pub3
+wait_for_subscription_sync($node_subscriber);
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
+wait_for_subscription_sync($node_subscriber);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub4
+wait_for_subscription_sync($node_subscriber);
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION
+wait_for_subscription_sync($node_subscriber);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub5
+wait_for_subscription_sync($node_subscriber);
+	CREATE TABLE test_part_a (a int, b int, c int) PARTITION BY LIST (a);
+# do the same thing on the subscriber (with the opposite column order)
+	CREATE TABLE test_part_a (b int, a int) PARTITION BY LIST (a);
+	CREATE PUBLICATION pub6 FOR TABLE test_part_a (b, a) WITH (publish_via_partition_root = true);
+	ALTER PUBLICATION pub6 ADD TABLE test_part_a_1 (a);
+	ALTER PUBLICATION pub6 ADD TABLE test_part_a_2 (b);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub6
+wait_for_subscription_sync($node_subscriber);
+	INSERT INTO test_part_a VALUES (1, 3);
+	INSERT INTO test_part_a VALUES (2, 4);
+is($node_subscriber->safe_psql('postgres',"SELECT a, b FROM test_part_a ORDER BY a, b"),
+   qq(1|3
+2|4),
+	CREATE PUBLICATION pub7 FOR TABLE test_part_b (a, b) WITH (publish_via_partition_root = true);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub7
+wait_for_subscription_sync($node_subscriber);
+	CREATE TABLE test_part_c (a int, b int, c int) PARTITION BY LIST (a);
+	CREATE TABLE test_part_c (a int, b int, c int) PARTITION BY LIST (a);
+# create a publication replicating data through partition root, with a column
+# filter on the root, and then add the partitions one by one with separate
+# column filters (but those are not applied)
+	CREATE PUBLICATION pub8 FOR TABLE test_part_c WITH (publish_via_partition_root = false);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_1 (a,c);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_2 (a,b);
+	DROP SUBSCRIPTION sub1;
+	CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub8;
+	INSERT INTO test_part_c VALUES (1, 3, 5);
+	INSERT INTO test_part_c VALUES (2, 4, 6);
+   qq(1||5
+2|4|),
+# create a publication not replicating data through partition root, without
+# a column filter on the root, and then add the partitions one by one with
+# separate column filters
+	DROP PUBLICATION pub8;
+	CREATE PUBLICATION pub8 FOR TABLE test_part_c WITH (publish_via_partition_root = false);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_1 (a);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_2 (a,b);
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
+	TRUNCATE test_part_c;
+wait_for_subscription_sync($node_subscriber);
+	TRUNCATE test_part_c;
+	INSERT INTO test_part_c VALUES (1, 3, 5);
+	INSERT INTO test_part_c VALUES (2, 4, 6);
+2|4|),
+	CREATE PUBLICATION pub9 FOR TABLE test_part_d (a) WITH (publish_via_partition_root = true);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub9
+wait_for_subscription_sync($node_subscriber);
+   qq(1|),
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_mix_1, pub_mix_2;
+wait_for_subscription_sync($node_subscriber);
+# drop unnecessary tables, so as not to interfere with the FOR ALL TABLES
+	DROP TABLE tab1, tab2, tab3, tab4, tab5, tab6, tab7, test_mix_1,
+			   test_part, test_part_a, test_part_b, test_part_c, test_part_d;
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_mix_3, pub_mix_4;
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
+wait_for_subscription_sync($node_subscriber);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_mix_5, pub_mix_6;
+wait_for_subscription_sync($node_subscriber);
+# published through partition root, we should only apply the column filter
+# defined for the whole table (not the partitions) - both during the initial
+# sync and when replicating changes. This is what we do for row filters.
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_root_true;
+wait_for_subscription_sync($node_subscriber);
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 2 Mar 2022 18:29:21 -0600

I applied this patch in my branch with CI hacks to show code coverage on
cirrus.
https://api.cirrus-ci.com/v1/artifact/task/6186186539532288/coverage/coverage/00-index.html

Eyeballing it looks good.  But GetActionsInPublication() isn't being hit at
all?

I think the queries in pg_dump should be written with the common portions of
the query outside the conditional.

-- 
Justin

---

From: Amit Kapila <[email-masked]>
Date: Fri, 4 Mar 2022 16:12:30 +0530

> far. There are various minor tweaks, but the main changes are:
...
> publications, or whenever we change the replica identity.

This handling is different from row filter work and I see problems
with it. The column list validation w.r.t primary key (default replica
identity) is missing. The handling of column list vs. partitions has
multiple problems: (a) In attach partition, the patch is just checking
ancestors for RI validation but what if the table being attached has
further subpartitions; (b) I think the current locking also seems to
have problems because it is quite possible that while it validates the
ancestors here, concurrently someone changes the column list. I think
it won't be enough to just change the locking mode because with the
current patch strategy during attach, we will be first taking locks
for child tables of current partition and then parent tables which can
pose deadlock hazards.

The columns list validation also needs to be done when we change
publication action.

There could be more similar problems which I might have missed. For
some of these (except for concurrency issues), my colleague Shi-San
has done testing and the results are below [1]. I feel we should do RI
vs. column list handling similar to row filter work (at one place) to
avoid all such hazards and possibly similar handling at various
places, there is a good chance that we will miss some places or make
mistakes that are not easy to catch. Do let me know if you think it
makes sense for me or one of the people who work on row filter patch
to try this (make the handling of RI checks similar to row filter
work) and then we can see if that turns out to be a simple way to deal
with all these problems?

Some other miscellaneous comments:
=============================
*
In get_rel_sync_entry(), the handling for partitioned tables doesn't
seem to be correct. It can publish a different set of columns based on
the order of publications specified in the subscription.

For example:
----
create table parent (a int, b int, c int) partition by range (a);
create table test_part1 (like parent);
alter table parent attach partition test_part1 for values from (1) to (10);

create publication pub for table parent(a) with (PUBLISH_VIA_PARTITION_ROOT);
create publication pub2 for table test_part1(b);
---

Now, depending on the order of publications in the list while defining
subscription, the column list will change
----
create subscription sub connection 'port=10000 dbname=postgres'
publication pub, pub2;

For the above, column list will be: (a)

create subscription sub connection 'port=10000 dbname=postgres'
publication pub2, pub;

For this one, the column list will be: (a, b)
----

To avoid this, the column list should be computed based on the final
publish_as_relid as we are doing for the row filter.

*
Fetching column filter info in tablesync.c is quite expensive. It
seems to be using four round-trips to get the complete info whereas
for row-filter we use just one round trip. I think we should try to
get both row filter and column filter info in just one round trip.

[1] -
Test-1:
The patch doesn't check when the primary key changes.

e.g.
-- publisher --
create table tbl(a int primary key, b int);
create publication pub for table tbl(a);
alter table tbl drop CONSTRAINT tbl_pkey;
alter table tbl add primary key (b);
insert into tbl values (1,1);

-- subscriber --
create table tbl(a int, b int);
create subscription sub connection 'port=5432 dbname=postgres' publication pub;
update tbl set b=1 where a=1;
alter table tbl add primary key (b);

-- publisher --
delete from tbl;

Column "b" is part of replica identity, but it is filtered, which
caused an error on the subscriber side.

ERROR:  publisher did not send replica identity column expected by the
logical replication target relation "public.tbl"
CONTEXT:  processing remote data during "DELETE" for replication
target relation "public.tbl" in transaction 724 at 2022-03-04
11:46:16.330892+08

Test-2: Partitioned table RI w.r.t column list.
2.1
Using "create table ... partition of".

e.g.
-- publisher --
create table parent (a int, b int) partition by range (a);
create publication pub for table parent(a)
with(publish_via_partition_root=true);
create table child partition of parent (primary key (a,b)) default;
insert into parent values (1,1);

-- subscriber --
create table parent (a int, b int) partition by range (a);
create table child partition of parent default;
create subscription sub connection 'port=5432 dbname=postgres'
publication pub; update child set b=1 where a=1;
alter table parent add primary key (a,b);

-- publisher --
delete from parent;

Column "b" is part of replica identity in the child table, but it is
filtered, which caused an error on the subscriber side.

ERROR:  publisher did not send replica identity column expected by the
logical replication target relation "public.parent"
CONTEXT:  processing remote data during "DELETE" for replication
target relation "public.parent" in transaction 723 at 2022-03-04
15:15:39.776949+08

2.2
It is likely that a table to be attached also has a partition.

e.g.
-- publisher --
create table t1 (a int, b int) partition by range (a);
create publication pub for table t1(b) with(publish_via_partition_root=true);
create table t2 (a int, b int) partition by range (a);
create table t3 (a int primary key, b int);
alter table t2 attach partition t3 default;
alter table t1 attach partition t2 default;
insert into t1 values (1,1);

-- subscriber --
create table t1 (a int, b int) partition by range (a);
create table t2 (a int, b int) partition by range (a);
create table t3 (a int, b int);
alter table t2 attach partition t3 default;
alter table t1 attach partition t2 default;
create subscription sub connection 'port=5432 dbname=postgres' publication pub;
update t1 set a=1 where b=1;
alter table t1 add primary key (a);

-- publisher --
delete from t1;

Column "a" is part of replica identity in table t3, but t3's ancestor
t1 is published with column "a" filtered, which caused an error on the
subscriber side.

ERROR:  publisher did not send replica identity column expected by the
logical replication target relation "public.t1"
CONTEXT:  processing remote data during "DELETE" for replication
target relation "public.t1" in transaction 726 at 2022-03-04
14:40:29.297392+08

3.
Using "alter publication pub set(publish='...'); "

e.g.
-- publisher --
create table tbl(a int primary key, b int); create publication pub for
table tbl(b) with(publish='insert'); insert into tbl values (1,1);

-- subscriber --
create table tbl(a int, b int);
create subscription sub connection 'port=5432 dbname=postgres' publication pub;

-- publisher --
alter publication pub set(publish='insert,update');

-- subscriber --
update tbl set a=1 where b=1;
alter table tbl add primary key (b);

-- publisher --
update tbl set a=2 where a=1;

Updates are replicated, and the column "a" is part of replica
identity, but it is filtered, which caused an error on the subscriber
side.

ERROR:  publisher did not send replica identity column expected by the
logical replication target relation "public.tbl"
CONTEXT:  processing remote data during "UPDATE" for replication
target relation "public.tbl" in transaction 723 at 2022-03-04
11:56:33.905843+08

-- 
With Regards,
Amit Kapila.

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 7 Mar 2022 16:18:04 +0100

On 3/4/22 11:42, Amit Kapila wrote:
> with it.

By different, I assume you mean I tried to enfoce the rules in ALTER
PUBLICATION and other ALTER commands, instead of when modifying the
data? OK, I reworked this to do the same thing as the row filtering patch.

> The column list validation w.r.t primary key (default replica
I believe those issues should be solved by adopting the same approach as
the row-filtering patch, right?

> mistakes that are not easy to catch.

I agree if both patches use the same approach, that would reduce the
risk of missing the handling in one place, etc.

> Do let me know if you think it makes sense for me or one of the 
> people who work on row filter patch to try this (make the handling of
> RI checks similar to row filter work) and then we can see if that
> turns out to be a simple way to deal with all these problems?
> 

If someone who is more familiar with the design conclusions from the row
filtering patch, that would be immensely useful. Especially now, when I
reworked it to the same approach as the row filtering patch.

> Some other miscellaneous comments:

Hmm, yeah. That seems like a genuine problem - it should not depend on
the order of publications in the subscription, I guess.

But is it an issue in the patch? Isn't that a pre-existing issue? AFAICS
the problem is that we initialize publish_as_relid=relid before the loop
over publications, and then just update it. So the first iteration
starts with relid, but the second iteration ends with whatever value is
set by the first iteration (e.g. the root).

So with the example you posted, we start with

  publish_as_relid = relid = test_part1

but then if the first publication is pubviaroot=true, we update it to
parent. And in the second iteration, we fail to find the column filter,
because "parent" (publish_as_relid) is not part of the pub2.

If we do it in the other order, we leave the publish_as_relid value as
is (and find the filter), and then update it in the second iteration
(and find the column filter too).

Now, this can be resolved by re-calculating the publish_as_relid from
scratch in each iteration (start with relid, then maybe update it). But
that's just half the story - the issue is there even without column
filters. Consider this example:

create table t (a int, b int, c int) partition by range (a);

create table t_1 partition of t for values from (1) to (10)
       partition by range (a);

create table t_2 partition of t_1 for values from (1) to (10);

create publication pub1 for table t(a)
  with (PUBLISH_VIA_PARTITION_ROOT);

create publication pub2 for table t_1(a)
  with (PUBLISH_VIA_PARTITION_ROOT);

Now, is you change subscribe to "pub1, pub2" and "pub2, pub1", we'll end
up with different publish_as_relid values (t or t_1). Which seems like
the same ambiguity issue.

> get both row filter and column filter info in just one round trip.

Maybe, but I really don't think this is an issue. The sync happens only
very rarely, and the rest of the sync (starting workers, copying data)
is likely way more expensive than this.

> target relation "public.tbl" in transaction 724 at 2022-03-04

AFAICS these issues should be resolved by the adoption of the row-filter
approach (i.e. it should fail the same way as for row filter).

regards

From 362723188a36b7e0147df7404fe64aa2e4f48bfb Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Sat, 26 Feb 2022 17:33:09 +0100
Subject: [PATCH 1/3] Verify changing WHERE condition for a publication

index 4e191c120ac..6d16600aaea 100644
@@ -417,15 +417,81 @@ LINE 1: ...ICATION testpub5 ADD TABLE testpub_rf_tbl1 WHERE (b < '2' CO...
index 5457c56b33f..c135a601a30 100644
@@ -206,15 +206,23 @@ CREATE COLLATION user_collation FROM "C";

From fd95ce9ffdf421ebc9ecd02c541c5d2f0c43cd97 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 1 Mar 2022 15:25:56 +0100
Subject: [PATCH 2/3] Test publication row filters in pg_dump tests

diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 3e55ff26f82..ae8c86a6e88 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2417,12 +2417,12 @@ my %tests = (
 		},
 	},

From 9c9d904c721d4d89ea475ea959f3dc96f16fa48e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 23 Feb 2022 21:15:18 +0100
Subject: [PATCH 3/3] Allow specifying column filters for logical replication

Author: Rahila Syed <[email-masked]>
Discussion: https://postgr.es/m/CAH2L28vddB_NFdRVpuyRBJEBWjz4BSyTB=[email-masked]
---
 doc/src/sgml/catalogs.sgml                  |  13 +
 doc/src/sgml/protocol.sgml                  |   4 +-
 doc/src/sgml/ref/alter_publication.sgml     |  23 +-
 doc/src/sgml/ref/create_publication.sgml    |  17 +-
 src/backend/catalog/pg_publication.c        | 260 ++++++
 src/backend/commands/publicationcmds.c      | 362 ++++++++-
 src/backend/commands/tablecmds.c            |  36 +-
 src/backend/executor/execReplication.c      |  19 +-
 src/backend/nodes/copyfuncs.c               |   1 +
 src/backend/nodes/equalfuncs.c              |   1 +
 src/backend/parser/gram.y                   |  60 +-
 src/backend/replication/logical/proto.c     |  55 +-
 src/backend/replication/logical/tablesync.c | 271 ++++++-
 src/backend/replication/pgoutput/pgoutput.c | 123 ++-
 src/backend/utils/cache/relcache.c          |  32 +
 src/bin/pg_dump/pg_dump.c                   |  47 +-
 src/bin/pg_dump/pg_dump.h                   |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl            |  60 ++
 src/bin/psql/describe.c                     |  40 +-
 src/include/catalog/pg_publication.h        |  14 +
 src/include/catalog/pg_publication_rel.h    |   1 +
 src/include/commands/publicationcmds.h      |   2 +
 src/include/nodes/parsenodes.h              |   4 +-
 src/include/replication/logicalproto.h      |   6 +-
 src/test/regress/expected/publication.out   | 375 +++++++++
 src/test/regress/sql/publication.sql        | 290 +++++++
 src/test/subscription/t/029_column_list.pl  | 836 ++++++++++++++++++++
 27 files changed, 2872 insertions(+), 81 deletions(-)
 create mode 100644 src/test/subscription/t/029_column_list.pl

index c51c4254a70..496593201b9 100644

 
+  <para>
 
   <para>
 
   <para>

 
+     <para>
 
index 25998fbb39b..4dab96265f8 100644
@@ -45,6 +45,9 @@
 
+static void publication_translate_columns(Relation targetrel, List *columns,
+										  int *natts, AttrNumber **attrs);
+
 /*
  * Check if relation can be in given publication and throws appropriate
  * error if not.
@@ -328,6 +331,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	Oid			relid = RelationGetRelid(targetrel);
 	Oid			pubreloid;
 	Publication *pub = GetPublication(pubid);
+	AttrNumber *attarray;
+	int			natts = 0;
+	int			i;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
@@ -355,6 +361,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 
 	check_publication_add_relation(targetrel);
 
+	/*
+	 * Translate column names to attnums and check the column list is valid.
+	 * We also deconstruct the bitmap into an array of attnums, for storing
+	 * in the catalog.
+	 */
+	publication_translate_columns(pri->relation, pri->columns,
+								  &natts, &attarray);
+
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
@@ -367,6 +381,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
 
+	/* Add column filter, if available */
+	if (pri->columns)
+	{
+		int2vector *prattrs;
+
@@ -382,6 +407,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 
+	/* Add dependency on the columns, if any are listed */
+	for (i = 0; i < natts; i++)
+	{
+		ObjectAddressSubSet(referenced, RelationRelationId, relid, attarray[i]);
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+	}
+	pfree(attarray);
+
 	/* Add dependency on the publication */
 	ObjectAddressSet(referenced, PublicationRelationId, pubid);
 	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
@@ -415,6 +448,155 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	return myself;
 }
 
+/*
+	int			i;
+		Form_pg_publication_rel	pubrel;
+		publication_translate_columns(targetrel, columns, &natts, &attarray);
+		pubrel = (Form_pg_publication_rel) GETSTRUCT(pubreltup);
+		ObjectAddressSet(myself, PublicationRelRelationId, pubrel->oid);
+		for (i = 0; i < natts; i++)
+publication_translate_columns(Relation targetrel, List *columns,
+							  int *natts, AttrNumber **attrs)
+	bms_free(set);
@@ -522,6 +704,84 @@ GetRelationPublications(Oid relid)
 	return result;
 }
 
+/*
+ * Gets a list of OIDs of all partial-column publications of the given
+ * relation, that is, those that specify a column list.
+ */
+List *
+GetRelationColumnPartialPublications(Oid relid)
+{
+	CatCList   *pubrellist;
+	List	   *pubs = NIL;
+	int			i;
+
+	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
+									 ObjectIdGetDatum(relid));
+	for (i = 0; i < pubrellist->n_members; i++)
+	{
+		HeapTuple	tup = &pubrellist->members[i]->tuple;
+		bool		isnull;
+		Form_pg_publication_rel	pubrel;
+
+		(void) SysCacheGetAttr(PUBLICATIONRELMAP, tup,
+							   Anum_pg_publication_rel_prattrs,
+							   &isnull);
+
+		/* no column filter for this publications/relation */
+		if (isnull)
+			continue;
+
+		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+
+		pubs = lappend_oid(pubs, pubrel->prpubid);
+	}
+
+	ReleaseSysCacheList(pubrellist);
+
+	return pubs;
+}
+
+
+/*
+ * For a relation in a publication that is known to have a non-null column
+ * list, return the list of attribute numbers that are in it.
+ */
+List *
+GetRelationColumnListInPublication(Oid relid, Oid pubid)
+{
+	HeapTuple	tup;
+	Datum		adatum;
+	bool		isnull;
+	ArrayType  *arr;
+	int			nelems;
+	int16	   *elems;
+	List	   *attnos = NIL;
+	int			i;
+
+	tup = SearchSysCache2(PUBLICATIONRELMAP,
+						  ObjectIdGetDatum(relid),
+						  ObjectIdGetDatum(pubid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for rel %u of publication %u", relid, pubid);
+
+	adatum = SysCacheGetAttr(PUBLICATIONRELMAP, tup,
+							 Anum_pg_publication_rel_prattrs, &isnull);
+	if (isnull)
+		elog(ERROR, "found unexpected null in pg_publication_rel.prattrs");
+
+	arr = DatumGetArrayTypeP(adatum);
+	nelems = ARR_DIMS(arr)[0];
+	elems = (int16 *) ARR_DATA_PTR(arr);
+
+	for (i = 0; i < nelems; i++)
+		attnos = lappend_oid(attnos, elems[i]);
+
+	ReleaseSysCache(tup);
+
+	return attnos;
+}
+
 /*
  * Gets list of relation oids for a publication.
  *
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 16b8661a1b7..fa1462ae546 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -367,6 +367,123 @@ contain_invalid_rfcolumn(Oid pubid, Relation relation, List *ancestors,
 	return result;
 }
 
+/*
+ * Check if all columns referenced in the column filter are part of the
+ * REPLICA IDENTITY index or not.
+ *
+ * Returns true if any invalid column is found.
+ */
+bool
+contain_invalid_cfcolumn(Oid pubid, Relation relation, List *ancestors,
+						 bool pubviaroot)
+{
+	HeapTuple	cftuple;
+	Oid			relid = RelationGetRelid(relation);
+	Oid			publish_as_relid = RelationGetRelid(relation);
+	bool		result = false;
+	Datum		cfdatum;
+	bool		cfisnull;
+
+	/*
+	 * For a partition, if pubviaroot is true, find the topmost ancestor that
+	 * is published via this publication as we need to use its column filter
+	 * for the changes.
+	 *
+	 * Note that even though the column filter used is for an ancestor, the
+	 * REPLICA IDENTITY used will be for the actual child table.
+	 */
+	if (pubviaroot && relation->rd_rel->relispartition)
+	{
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors);
+
+		if (!OidIsValid(publish_as_relid))
+			publish_as_relid = relid;
+	}
+
+	cftuple = SearchSysCache2(PUBLICATIONRELMAP,
+							  ObjectIdGetDatum(publish_as_relid),
+							  ObjectIdGetDatum(pubid));
+
+	if (!HeapTupleIsValid(cftuple))
+		return false;
+
+	cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+							  Anum_pg_publication_rel_prattrs,
+							  &cfisnull);
+
+	if (!cfisnull)
+	{
+		int	x;
+		Bitmapset  *idattrs;
+		Bitmapset  *columns = NULL;
+
+		/* With REPLICA IDENTITY FULL, no column filter is allowed. */
+		if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
+			result = true;
+
+		{
+			ArrayType  *arr;
+			int			nelems;
+			int16	   *elems;
+
+			arr = DatumGetArrayTypeP(cfdatum);
+			nelems = ARR_DIMS(arr)[0];
+			elems = (int16 *) ARR_DATA_PTR(arr);
+
+			/* XXX is there a danger of memory leak here? beware */
+			for (int i = 0; i < nelems; i++)
+				columns = bms_add_member(columns, elems[i]);
+		}
+
+		/* Remember columns that are part of the REPLICA IDENTITY */
+		idattrs = RelationGetIndexAttrBitmap(relation,
+											 INDEX_ATTR_BITMAP_IDENTITY_KEY);
+
+		/*
+		 * Attnums in the bitmap returned by RelationGetIndexAttrBitmap are
+		 * offset (to handle system columns the usual way), while column filter
+		 * does not use offset, so we can't do bms_is_subset(). Instead, we have
+		 * to loop over the idattrs and check all of them are in the filter.
+		 */
+		x = -1;
+		while ((x = bms_next_member(idattrs, x)) >= 0)
+		{
+			AttrNumber	attnum = (x + FirstLowInvalidHeapAttributeNumber);
+
+			/*
+			 * If pubviaroot is true, we are validating the row filter of the
+			 * parent table, but the bitmap contains the replica identity
+			 * information of the child table. So, get the column number of the
+			 * child table as parent and child column order could be different.
+			 */
+			if (pubviaroot)
+			{
+				/* attnum is for child table */
+				char   *colname = get_attname(relid, attnum, false);
+
+				/*
+				 * Determine the matching attnum in parent (because the column
+				 * filter is defined on the parent).
+				 */
+				attnum = get_attnum(publish_as_relid, colname);
+			}
+
+			if (!bms_is_member(attnum, columns))
+			{
+				result = true;
+				break;
+			}
+		}
+
+		bms_free(idattrs);
+		bms_free(columns);
+	}
+
+	ReleaseSysCache(cftuple);
+
+	return result;
+}
+
 /* check_functions_in_node callback */
 static bool
 contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -608,6 +725,45 @@ TransformPubWhereClauses(List *tables, const char *queryString,
 	}
 }
 
+
+/*
@@ -724,6 +880,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 
+			TransformPubColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
+
 			PublicationAddTables(puboid, rels, true, NULL);
 			CloseTableList(rels);
 		}
@@ -754,6 +913,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	return myself;
 }
 
+/*
+	 * Invalidate relcache for this relation, to force rebuilding the
+	 * publication description.
+	CacheInvalidateRelcache(urel);
@@ -783,8 +988,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 
 	/*
 	 * If the publication doesn't publish changes via the root partitioned
-	 * table, the partition's row filter will be used. So disallow using WHERE
-	 * clause on partitioned table in this case.
+	 * table, the partition's row and column filter will be used. So disallow
+	 * using WHERE clause and column filters on partitioned table in this case.
 	 */
 	if (!pubform->puballtables && publish_via_partition_root_given &&
 		!publish_via_partition_root)
@@ -792,7 +997,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		/*
 		 * Lock the publication so nobody else can do anything with it. This
 		 * prevents concurrent alter to add partitioned table(s) with WHERE
-		 * clause(s) which we don't allow when not publishing via root.
+		 * clause(s) and/or column filters which we don't allow when not
+		 * publishing via root.
 		 */
 		LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
 						   AccessShareLock);
@@ -804,13 +1010,21 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		{
 			HeapTuple	rftuple;
 			Oid			relid = lfirst_oid(lc);
+			bool		has_column_filter;
+			bool		has_row_filter;
 
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
 									  ObjectIdGetDatum(relid),
 									  ObjectIdGetDatum(pubform->oid));
 
+			has_row_filter
+				= !heap_attisnull(rftuple, Anum_pg_publication_rel_prqual, NULL);
+
+			has_column_filter
+				= !heap_attisnull(rftuple, Anum_pg_publication_rel_prattrs, NULL);
+
 			if (HeapTupleIsValid(rftuple) &&
-				!heap_attisnull(rftuple, Anum_pg_publication_rel_prqual, NULL))
+				(has_row_filter || has_column_filter))
 			{
 				HeapTuple	tuple;
 
@@ -819,7 +1033,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 				{
 					Form_pg_class relform = (Form_pg_class) GETSTRUCT(tuple);
 
-					if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+					if ((relform->relkind == RELKIND_PARTITIONED_TABLE) &&
+						has_row_filter)
 						ereport(ERROR,
 								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 								 errmsg("cannot set %s for publication \"%s\"",
@@ -830,6 +1045,18 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 										   NameStr(relform->relname),
 										   "publish_via_partition_root")));
 
+					if ((relform->relkind == RELKIND_PARTITIONED_TABLE) &&
+						has_column_filter)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("cannot set %s for publication \"%s\"",
+										"publish_via_partition_root = false",
+										stmt->pubname),
+								 errdetail("The publication contains a column filter for a partitioned table \"%s\" "
+										   "which is not allowed when %s is false.",
+										   NameStr(relform->relname),
+										   "publish_via_partition_root")));
+
 					ReleaseSysCache(tuple);
 				}
 
@@ -838,6 +1065,16 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		}
 	}
 
+	/*
@@ -975,10 +1212,20 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,

@@ -991,6 +1238,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,

@@ -1002,32 +1251,85 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,

+			/*

 
 			foreach(newlc, rels)
 			{

 				/*
@@ -1037,7 +1339,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
@@ -1056,6 +1359,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
@@ -1117,7 +1421,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
@@ -1140,6 +1444,10 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 
 /*
@@ -1402,6 +1710,7 @@ OpenTableList(List *tables)
 	List	   *rels = NIL;
 	ListCell   *lc;
 	List	   *relids_with_rf = NIL;
+	List	   *relids_with_cf = NIL;
 
 	/*
 	 * Open, share-lock, and check all the explicitly-specified relations
@@ -1436,6 +1745,13 @@ OpenTableList(List *tables)
 						 errmsg("conflicting or redundant WHERE clauses for table \"%s\"",
 								RelationGetRelationName(rel))));
 
+			/* Disallow duplicate tables if there are any with column filters. */
+			if (t->columns || list_member_oid(relids_with_cf, myrelid))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+						 errmsg("conflicting or redundant column filters for table \"%s\"",
+								RelationGetRelationName(rel))));
+
 			table_close(rel, ShareUpdateExclusiveLock);
 			continue;
 		}
@@ -1443,12 +1759,16 @@ OpenTableList(List *tables)
 		pub_rel = palloc(sizeof(PublicationRelInfo));
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
+		pub_rel->columns = t->columns;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
 		if (t->whereClause)
 			relids_with_rf = lappend_oid(relids_with_rf, myrelid);
 
+		if (t->columns)
+			relids_with_cf = lappend_oid(relids_with_cf, myrelid);
+
 		/*
 		 * Add children of this rel, if requested, so that they too are added
 		 * to the publication.  A partitioned table can't have any inheritance
@@ -1488,6 +1808,18 @@ OpenTableList(List *tables)
 								 errmsg("conflicting or redundant WHERE clauses for table \"%s\"",
 										RelationGetRelationName(rel))));
 
+					/*
+					 * We don't allow to specify column filter for both parent
+					 * and child table at the same time as it is not very
+					 * clear which one should be given preference.
+					 */
+					if (childrelid != myrelid &&
+						(t->columns || list_member_oid(relids_with_cf, childrelid)))
+						ereport(ERROR,
+								(errcode(ERRCODE_DUPLICATE_OBJECT),
+								 errmsg("conflicting or redundant column fiters for table \"%s\"",
+										RelationGetRelationName(rel))));
+
 					continue;
 				}
 
@@ -1497,11 +1829,16 @@ OpenTableList(List *tables)
 				pub_rel->relation = rel;
 
 				if (t->whereClause)
 					relids_with_rf = lappend_oid(relids_with_rf, childrelid);
+
+				if (t->whereClause)
+					relids_with_cf = lappend_oid(relids_with_cf, childrelid);
 			}
 		}
 	}
@@ -1610,6 +1947,11 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 		Relation	rel = pubrel->relation;
 		Oid			relid = RelationGetRelid(rel);
 
+		if (pubrel->columns)
+			ereport(ERROR,
index dc5872f988c..a9fd0f0c895 100644
 
 	ReleaseSysCache(tuple);
 
+	/*
 
 	if (children)
 	{

diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index de106d767d1..d87be2d4775 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -573,9 +573,6 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd)
 	if (cmd != CMD_UPDATE && cmd != CMD_DELETE)
 		return;
 
-	if (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
-		return;
-
 	/*
 	 * It is only safe to execute UPDATE/DELETE when all columns, referenced
 	 * in the row filters from publications which the relation is in, are
@@ -595,17 +592,33 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd)
 				 errmsg("cannot update table \"%s\"",
 						RelationGetRelationName(rel)),
 				 errdetail("Column used in the publication WHERE expression is not part of the replica identity.")));
+	else if (cmd == CMD_UPDATE && !pubdesc.cf_valid_for_update)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+				 errmsg("cannot update table \"%s\"",
+						RelationGetRelationName(rel)),
+				 errdetail("Column filter used by the publication does not cover the replica identity.")));
 	else if (cmd == CMD_DELETE && !pubdesc.rf_valid_for_delete)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
 				 errmsg("cannot delete from table \"%s\"",
 						RelationGetRelationName(rel)),
 				 errdetail("Column used in the publication WHERE expression is not part of the replica identity.")));
+	else if (cmd == CMD_DELETE && !pubdesc.cf_valid_for_delete)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+				 errmsg("cannot delete from table \"%s\"",
+						RelationGetRelationName(rel)),
+				 errdetail("Column filter used by the publication does not cover the replica identity.")));
 
 	/* If relation has replica identity we are always good. */
 	if (OidIsValid(RelationGetReplicaIndex(rel)))
 		return;
 
+	/* REPLICA IDENTITY FULL is also good for UPDATE/DELETE. */
+	if (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
+		return;
+
 	/*
 	 * This is either UPDATE OR DELETE and there is no replica identity.
 	 *
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index d4f8455a2bd..a504437873f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4850,6 +4850,7 @@ _copyPublicationTable(const PublicationTable *from)

 	return newnode;
 }
 
 	return true;
 }
 
index c9b0eeefd7e..1e8785ff9a5 100644
@@ -29,16 +29,21 @@

 static void logicalrep_write_namespace(StringInfo out, Oid nspid);
 static const char *logicalrep_read_namespace(StringInfo in);
 
+#define	ColumnInFilter(columns, attnum) \
+		(((columns) == NULL) || (bms_is_member((attnum), (columns))))
+
+
 /*
  * Write BEGIN to the output stream.
  */
@@ -398,7 +403,7 @@ logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn)
  */
 void
 logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
-						TupleTableSlot *newslot, bool binary)
+						TupleTableSlot *newslot, bool binary, Bitmapset *columns)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_INSERT);
 
@@ -410,7 +415,7 @@ logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
 	pq_sendint32(out, RelationGetRelid(rel));

 /*
@@ -443,7 +448,7 @@ logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup)
 void
 
@@ -464,11 +469,11 @@ logicalrep_write_update(StringInfo out, TransactionId xid, Relation rel,

 /*
@@ -537,7 +542,7 @@ logicalrep_write_delete(StringInfo out, TransactionId xid, Relation rel,
 	else
 		pq_sendbyte(out, 'K');	/* old key follows */

 /*
@@ -652,7 +657,8 @@ logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
  * Write relation description to the output stream.
  */
 void
-logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
+logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel,
+					 Bitmapset *columns)
 {
 	char	   *relname;
 
@@ -674,7 +680,7 @@ logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
 	pq_sendbyte(out, rel->rd_rel->relreplident);

 /*
@@ -751,7 +757,7 @@ logicalrep_read_typ(StringInfo in, LogicalRepTyp *ltyp)
  */
 static void
 logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
-					   bool binary)
+					   bool binary, Bitmapset *columns)
 {
 	TupleDesc	desc;
 	Datum	   *values;
@@ -763,7 +769,13 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 
 	for (i = 0; i < desc->natts; i++)
 	{
-		if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
+		Form_pg_attribute att = TupleDescAttr(desc, i);
+
+		if (att->attisdropped || att->attgenerated)
+			continue;
+
+		/* skip columns not included in the column filter */
+		if (!ColumnInFilter(columns, att->attnum))
 			continue;
 		nliveatts++;
 	}
@@ -783,6 +795,10 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
+		/* skip columns not included in the column filter */
+		if (!ColumnInFilter(columns, att->attnum))
+			continue;
+
 		if (isnull[i])
 		{
+		/* skip columns not included in the column filter */
+		if (!ColumnInFilter(columns, att->attnum))
 
+		/* skip columns not included in the column filter */
+		if (!ColumnInFilter(columns, att->attnum))
+			continue;
+
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;

 
 
+	/*

index ea57a0477f0..07cdfc1d8c0 100644
 
@@ -85,7 +86,8 @@ static List *LoadPublications(List *pubnames);
 static void publication_invalidation_cb(Datum arg, int cacheid,
 										uint32 hashvalue);
 static void send_relation_and_attrs(Relation relation, TransactionId xid,
-									LogicalDecodingContext *ctx);
+									LogicalDecodingContext *ctx,
+									Bitmapset *columns);
 static void send_repl_origin(LogicalDecodingContext *ctx,
 							 RepOriginId origin_id, XLogRecPtr origin_lsn,
 							 bool send_origin);
@@ -93,6 +95,8 @@ static void send_repl_origin(LogicalDecodingContext *ctx,
 /*
  * Only 3 publication actions are used for row filtering ("insert", "update",
  * "delete"). See RelationSyncEntry.exprstate[].
+ *
+ * FIXME Do we need something similar for column filters?
  */
 enum RowFilterPubAction
 {
@@ -164,6 +168,13 @@ typedef struct RelationSyncEntry
 	 * having identical TupleDesc.
 	 */
 	AttrMap    *attrmap;
+
+	/*
+	 * Columns included in the publication, or NULL if all columns are
+	 * included implicitly.  Note that the attnums in this bitmap are not
+	 * shifted by FirstLowInvalidHeapAttributeNumber.
+	 */
+	Bitmapset  *columns;
 } RelationSyncEntry;
 
 /* Map used to remember which relation schemas we sent. */
@@ -603,11 +614,11 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 	{
 		Relation	ancestor = RelationIdGetRelation(relentry->publish_as_relid);

 
 	if (in_streaming)
 		set_schema_sent_in_streamed_txn(relentry, topxid);
@@ -620,7 +631,8 @@ maybe_send_schema(LogicalDecodingContext *ctx,
  */
 static void
 send_relation_and_attrs(Relation relation, TransactionId xid,
-						LogicalDecodingContext *ctx)
+						LogicalDecodingContext *ctx,
+						Bitmapset *columns)
 {
 	TupleDesc	desc = RelationGetDescr(relation);
 	int			i;
@@ -643,13 +655,17 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		if (att->atttypid < FirstGenbkiObjectId)
 			continue;

 
@@ -1224,7 +1240,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
@@ -1278,11 +1294,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
@@ -1731,6 +1749,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
@@ -1775,6 +1794,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->schema_sent = false;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
+		bms_free(entry->columns);
+		entry->columns = NULL;
 		entry->pubactions.pubinsert = false;
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
@@ -1807,13 +1828,16 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		/*
 
 			if (pub->alltables)
 			{
@@ -1824,8 +1848,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 			if (!publish)
 			{
-				bool		ancestor_published = false;
-
 				/*
 				 * For a partition, check if any of the ancestors are
 				 * published.  If so, note down the topmost ancestor that is
@@ -1855,6 +1877,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			}
 
 			/*
@@ -1867,6 +1892,80 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
+				/*
+				 * This might be FOR ALL TABLES or FOR ALL TABLES IN SCHEMA
+				 * publication, in which case there are no column lists, and
+				 * we treat that as all_columns=true.
+				 */
+				if (pub->alltables ||
+					list_member_oid(schemaPubids, pub->oid))
+				{
+					all_columns = true;
+					bms_free(entry->columns);
+					entry->columns = NULL;
+				}
+
+				/*
+				 * Obtain columns published by this publication, and add them
+				 * to the list for this rel.  Note that if at least one
+				 * publication has an empty column list, that means to publish
+				 * everything; so if we saw a publication that includes all
+				 * columns, skip this.
+				 *
+				 * FIXME This fails to consider column filters defined in
+				 * FOR ALL TABLES and FOR ALL TABLES IN SCHEMA publications.
+				 * We need to check those too.
+				 */
+				if (!all_columns)
+				{
+					HeapTuple	pub_rel_tuple;
+
+					pub_rel_tuple = SearchSysCache2(PUBLICATIONRELMAP,
+													ObjectIdGetDatum(publish_as_relid),
+													ObjectIdGetDatum(pub->oid));
+
+					if (HeapTupleIsValid(pub_rel_tuple))
+					{
+						Datum		pub_rel_cols;
+						bool		isnull;
+
+						pub_rel_cols = SysCacheGetAttr(PUBLICATIONRELMAP,
+													   pub_rel_tuple,
+													   Anum_pg_publication_rel_prattrs,
+													   &isnull);
+						if (isnull)
+						{
+							/*
+							 * If we see a publication with no column filter, it
+							 * means we need to publish all columns, so reset the
+							 * list and ignore further ones.
+							 */
+							all_columns = true;
+							bms_free(entry->columns);
+							entry->columns = NULL;
+						}
+						else
+						{
+							ArrayType  *arr;
+							int			nelems;
+							int16	   *elems;
+
+							arr = DatumGetArrayTypeP(pub_rel_cols);
+							nelems = ARR_DIMS(arr)[0];
+							elems = (int16 *) ARR_DATA_PTR(arr);
+
+							/* XXX is there a danger of memory leak here? beware */
+							oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+							for (int i = 0; i < nelems; i++)
+								entry->columns = bms_add_member(entry->columns,
+																elems[i]);
+							MemoryContextSwitchTo(oldctx);
+						}
+
+						ReleaseSysCache(pub_rel_tuple);
+					}
+				}
+
 				rel_publications = lappend(rel_publications, pub);
 			}
 		}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce5729..82e595396e3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5553,6 +5553,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		memset(pubdesc, 0, sizeof(PublicationDesc));
 		pubdesc->rf_valid_for_update = true;
 		pubdesc->rf_valid_for_delete = true;
+		pubdesc->cf_valid_for_update = true;
+		pubdesc->cf_valid_for_delete = true;
 		return;
 	}
 
@@ -5565,6 +5567,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	memset(pubdesc, 0, sizeof(PublicationDesc));
 	pubdesc->rf_valid_for_update = true;
 	pubdesc->rf_valid_for_delete = true;
+	pubdesc->cf_valid_for_update = true;
+	pubdesc->cf_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
 	puboids = GetRelationPublications(relid);
@@ -5625,6 +5629,24 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 				pubdesc->rf_valid_for_delete = false;
 		}
 
+		/*
+		 * Check if all columns referenced in the column filter are part of
+		 * the REPLICA IDENTITY index or not.
+		 *
+		 * If the publication is FOR ALL TABLES then it means the table has no
+		 * column filters and we can skip the validation.
+		 */
+		if (!pubform->puballtables &&
+			(pubform->pubupdate || pubform->pubdelete) &&
+			contain_invalid_cfcolumn(pubid, relation, ancestors,
+									 pubform->pubviaroot))
+		{
+			if (pubform->pubupdate)
+				pubdesc->cf_valid_for_update = false;
+			if (pubform->pubdelete)
+				pubdesc->cf_valid_for_delete = false;
+		}
+
 		ReleaseSysCache(tup);
 
 		/*
@@ -5636,6 +5658,16 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
 			!pubdesc->rf_valid_for_update && !pubdesc->rf_valid_for_delete)
 			break;
+
+		/*
+		 * If we know everything is replicated and the column filter is invalid
+		 * for update and delete, there is no point to check for other
+		 * publications.
+		 */
+		if (pubdesc->pubactions.pubinsert && pubdesc->pubactions.pubupdate &&
+			pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
+			!pubdesc->cf_valid_for_update && !pubdesc->cf_valid_for_delete)
+			break;
 	}
 
 	if (relation->rd_pubdesc)

 

 
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index ae8c86a6e88..d4d92465c49 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2428,6 +2428,28 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
@@ -2778,6 +2800,44 @@ my %tests = (

 
 
 		printTableAddFooter(cont, buf->data);
 	}
@@ -6021,11 +6040,22 @@ describePublications(const char *pattern)
 			printfPQExpBuffer(&buf,
 							  "SELECT n.nspname, c.relname");
 			if (pset.sversion >= 150000)
+			{
 				appendPQExpBufferStr(&buf,
 									 ", pg_get_expr(pr.prqual, c.oid)");
+				appendPQExpBufferStr(&buf,
+									 ", (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+									 "     pg_catalog.array_to_string("
+									 "      ARRAY(SELECT attname\n"
+									 "              FROM\n"
+									 "                pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+									 "                pg_catalog.pg_attribute\n"
+									 "        WHERE attrelid = c.oid AND attnum = prattrs[s]), ', ')\n"
+									 "       ELSE NULL END)");
+			}
 			else
 				appendPQExpBufferStr(&buf,
-									 ", NULL");
+									 ", NULL, NULL");
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index ba72e62e614..b58f85ede27 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -85,6 +85,14 @@ typedef struct PublicationDesc
 	 */
 	bool		rf_valid_for_update;
 	bool		rf_valid_for_delete;
+
+	/*
+	 * true if the columns referenced in column filters which are used for UPDATE
+	 * or DELETE are part of the replica identity or the publication actions
+	 * do not include UPDATE or DELETE.
+	 */
+	bool		cf_valid_for_update;
+	bool		cf_valid_for_delete;
 } PublicationDesc;
 
 typedef struct Publication
@@ -100,6 +108,7 @@ typedef struct PublicationRelInfo
 {
 	Relation	relation;
 	Node	   *whereClause;
+	List	   *columns;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
@@ -123,8 +132,11 @@ typedef enum PublicationPartOpt
 } PublicationPartOpt;
 
@@ -142,6 +154,8 @@ extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri

 
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 7813cbcb6bb..08d14ca7245 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -33,5 +33,7 @@ extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool contain_invalid_rfcolumn(Oid pubid, Relation relation,
 									 List *ancestors, bool pubviaroot);
+extern bool contain_invalid_cfcolumn(Oid pubid, Relation relation,
+									 List *ancestors, bool pubviaroot);
 
 #endif							/* PUBLICATIONCMDS_H */
 
 /*
 
 typedef struct AlterPublicationStmt
diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h
index 4d2c881644a..a771ab8ff33 100644
--- a/src/include/replication/logicalproto.h
+++ b/src/include/replication/logicalproto.h
@@ -209,12 +209,12 @@ extern char *logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn);
 extern void logicalrep_write_insert(StringInfo out, TransactionId xid,
 									Relation rel,
 									TupleTableSlot *newslot,
-									bool binary);
+									bool binary, Bitmapset *columns);
 extern LogicalRepRelId logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup);
 extern void logicalrep_write_update(StringInfo out, TransactionId xid,
 									Relation rel,
 									TupleTableSlot *oldslot,
-									TupleTableSlot *newslot, bool binary);
+									TupleTableSlot *newslot, bool binary, Bitmapset *columns);
 extern LogicalRepRelId logicalrep_read_update(StringInfo in,
 											  bool *has_oldtuple, LogicalRepTupleData *oldtup,
 											  LogicalRepTupleData *newtup);
@@ -231,7 +231,7 @@ extern List *logicalrep_read_truncate(StringInfo in,
 extern void logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
 									 bool transactional, const char *prefix, Size sz, const char *message);
 extern void logicalrep_write_rel(StringInfo out, TransactionId xid,
-								 Relation rel);
+								 Relation rel, Bitmapset *columns);
 extern LogicalRepRelation *logicalrep_read_rel(StringInfo in);
 extern void logicalrep_write_typ(StringInfo out, TransactionId xid,
 								 Oid typoid);
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 6d16600aaea..152f19fb42b 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -679,6 +679,372 @@ DROP TABLE rf_tbl_abcd_pk;
 DROP TABLE rf_tbl_abcd_nopk;
 DROP TABLE rf_tbl_abcd_part_pk;
 -- ======================================================
+-- fail - duplicate tables are not allowed if that table has any column filters
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1 (a), testpub_tbl1 WITH (publish = 'insert');
+ERROR:  conflicting or redundant column filters for table "testpub_tbl1"
+CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1, testpub_tbl1 (a) WITH (publish = 'insert');
+ERROR:  conflicting or redundant column filters for table "testpub_tbl1"
+RESET client_min_messages;
+-- test for column filters
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
+CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
+RESET client_min_messages;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
+	d int generated always as (a + length(b)) stored);
+-- error: column "x" does not exist
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
+ERROR:  column "x" of relation "testpub_tbl5" does not exist
+-- error: replica identity "a" not included in the column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);
+UPDATE testpub_tbl5 SET a = 1;
+ERROR:  cannot update table "testpub_tbl5"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+-- error: generated column "d" can't be in list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
+ERROR:  cannot reference generated column "d" in publication column list
+-- error: system attributes "ctid" not allowed in column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, ctid);
+ERROR:  cannot reference system column "ctid" in publication column list
+-- ok
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
+ERROR:  cannot drop column "c" because it is part of publication "testpub_fortable"
+HINT:  Specify CASCADE or use ALTER PUBLICATION to remove the column from the publication.
+-- ok: for insert-only publication, the filter is arbitrary
+ALTER PUBLICATION testpub_fortable_insert ADD TABLE testpub_tbl5 (b, c);
+/* not all replica identities are good enough */
+CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
+ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
+-- error: replica identity (b,c) is covered by column filter (a, c)
+UPDATE testpub_tbl5 SET a = 1;
+ERROR:  cannot update table "testpub_tbl5"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+-- error: change the replica identity to "b", and then filter (a, c) fails
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+-- error: replica identity (b,c) is not covered by column filter (a,c)
+UPDATE testpub_tbl5 SET a = 1;
+ERROR:  cannot update table "testpub_tbl5"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+/* But if upd/del are not published, it works OK */
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
+RESET client_min_messages;
+ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
+\dRp+ testpub_table_ins
+                               Publication testpub_table_ins
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | f       | f       | t         | f
+Tables:
+    "public.testpub_tbl5" (a)
+
+-- with REPLICA IDENTITY FULL, column filters are not allowed
+CREATE TABLE testpub_tbl6 (a int, b text, c text);
+ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);
+UPDATE testpub_tbl6 SET a = 1;
+ERROR:  cannot update table "testpub_tbl6"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl6;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
+UPDATE testpub_tbl6 SET a = 1;
+ALTER PUBLICATION testpub_fortable ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);
+UPDATE testpub_tbl6 SET a = 1;
+ERROR:  cannot update table "testpub_tbl6"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+-- make sure changing the column filter is updated in SET TABLE
+CREATE TABLE testpub_tbl7 (a int primary key, b text, c text);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+                                Table "public.testpub_tbl7"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           | not null |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+ c      | text    |           |          |         | extended |              | 
+Indexes:
+    "testpub_tbl7_pkey" PRIMARY KEY, btree (a)
+Publications:
+    "testpub_fortable" (a, b)
+
+-- ok: we'll skip this table
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+                                Table "public.testpub_tbl7"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           | not null |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+ c      | text    |           |          |         | extended |              | 
+Indexes:
+    "testpub_tbl7_pkey" PRIMARY KEY, btree (a)
+Publications:
+    "testpub_fortable" (a, b)
+
+-- ok: update the column filter
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c);
+\d+ testpub_tbl7
+                                Table "public.testpub_tbl7"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | integer |           | not null |         | plain    |              | 
+ b      | text    |           |          |         | extended |              | 
+ c      | text    |           |          |         | extended |              | 
+Indexes:
+    "testpub_tbl7_pkey" PRIMARY KEY, btree (a)
+Publications:
+    "testpub_fortable" (a, c)
+
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 0);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 1);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (b);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+-- ok: column filter covers both "a" and "b"
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
+RESET client_min_messages;
+-- ok: the same thing, but try plain ADD TABLE
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+UPDATE testpub_tbl8 SET a = 1;
+-- failure: column filter does not cover replica identity for the second partition
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+UPDATE testpub_tbl8 SET a = 1;
+ERROR:  cannot update table "testpub_tbl8_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+-- failure: one of the partitions has REPLICA IDENTITY FULL
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+UPDATE testpub_tbl8 SET a = 1;
+ERROR:  cannot update table "testpub_tbl8_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+-- add table and then try changing replica identity
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- failure: replica identity full can't be used with a column filter
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+UPDATE testpub_tbl8 SET a = 1;
+ERROR:  cannot update table "testpub_tbl8_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+-- failure: replica identity has to be covered by the column filter
+ALTER TABLE testpub_tbl8_1 DROP CONSTRAINT testpub_tbl8_1_pkey;
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+UPDATE testpub_tbl8 SET a = 1;
+ERROR:  cannot update table "testpub_tbl8_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+DROP TABLE testpub_tbl8;
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+-- ok: attaching first partition works, because (a) is in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_0 FOR VALUES WITH (modulus 2, remainder 0);
+-- failure: second partition has replica identity (c), which si not in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_1 FOR VALUES WITH (modulus 2, remainder 1);
+UPDATE testpub_tbl8 SET a = 1;
+ERROR:  cannot update table "testpub_tbl8_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+-- failure: changing replica identity to FULL for partition fails, because
+-- of the column filter on the parent
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY FULL;
+UPDATE testpub_tbl8 SET a = 1;
+ERROR:  cannot update table "testpub_tbl8_0"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+DROP TABLE testpub_tbl5, testpub_tbl6, testpub_tbl7, testpub_tbl8, testpub_tbl8_1;
+DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_filter;
+-- ======================================================
+-- Test combination of column and row filter
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_both_filters;
+RESET client_min_messages;
+CREATE TABLE testpub_tbl_both_filters (a int, b int, c int, PRIMARY KEY (a,c));
+ALTER TABLE testpub_tbl_both_filters REPLICA IDENTITY USING INDEX testpub_tbl_both_filters_pkey;
+ALTER PUBLICATION testpub_both_filters ADD TABLE testpub_tbl_both_filters (a,c) WHERE (c != 1);
+\dRp+ testpub_both_filters
+                              Publication testpub_both_filters
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "public.testpub_tbl_both_filters" (a, c) WHERE (c <> 1)
+
+\d+ testpub_tbl_both_filters
+                         Table "public.testpub_tbl_both_filters"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+Indexes:
+    "testpub_tbl_both_filters_pkey" PRIMARY KEY, btree (a, c) REPLICA IDENTITY
+Publications:
+    "testpub_both_filters" (a, c) WHERE (c <> 1)
+
+DROP TABLE testpub_tbl_both_filters;
+DROP PUBLICATION testpub_both_filters;
+-- ======================================================
+-- More column filter tests for validating column references
+CREATE TABLE rf_tbl_abcd_nopk(a int, b int, c int, d int);
+CREATE TABLE rf_tbl_abcd_pk(a int, b int, c int, d int, PRIMARY KEY(a,b));
+CREATE TABLE rf_tbl_abcd_part_pk (a int PRIMARY KEY, b int) PARTITION by RANGE (a);
+CREATE TABLE rf_tbl_abcd_part_pk_1 (b int, a int PRIMARY KEY);
+ALTER TABLE rf_tbl_abcd_part_pk ATTACH PARTITION rf_tbl_abcd_part_pk_1 FOR VALUES FROM (1) TO (10);
+-- Case 1. REPLICA IDENTITY DEFAULT (means use primary key or nothing)
+-- 1a. REPLICA IDENTITY is DEFAULT and table has a PK.
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub6 FOR TABLE rf_tbl_abcd_pk (a, b);
+RESET client_min_messages;
+-- ok - (a,b) coverts all PK cols
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c);
+-- ok - (a,b,c) coverts all PK cols
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
+-- fail - "b" is missing from the column filter
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (b);
+-- fail - "a" is missing from the column filter
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+-- 1b. REPLICA IDENTITY is DEFAULT and table has no PK
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
+-- ok - there's no replica identity, so any column filter works
+-- note: it fails anyway, just a bit later because UPDATE requires RI
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_nopk" because it does not have a replica identity and publishes updates
+HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
+-- Case 2. REPLICA IDENTITY FULL
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY FULL;
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
+-- fail - with REPLICA IDENTITY FULL no column filter is allowed
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a, b, c, d);
+-- fail - with REPLICA IDENTITY FULL no column filter is allowed
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_nopk"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+-- Case 3. REPLICA IDENTITY NOTHING
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY NOTHING;
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
+-- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- it still fails later because without RI we can't replicate updates
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk" because it does not have a replica identity and publishes updates
+HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c, d);
+-- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- it still fails later because without RI we can't replicate updates
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk" because it does not have a replica identity and publishes updates
+HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (d);
+-- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- it still fails later because without RI we can't replicate updates
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_nopk" because it does not have a replica identity and publishes updates
+HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
+-- Case 4. REPLICA IDENTITY INDEX
+ALTER TABLE rf_tbl_abcd_pk ALTER COLUMN c SET NOT NULL;
+CREATE UNIQUE INDEX idx_abcd_pk_c ON rf_tbl_abcd_pk(c);
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY USING INDEX idx_abcd_pk_c;
+ALTER TABLE rf_tbl_abcd_nopk ALTER COLUMN c SET NOT NULL;
+CREATE UNIQUE INDEX idx_abcd_nopk_c ON rf_tbl_abcd_nopk(c);
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY USING INDEX idx_abcd_nopk_c;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
+-- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
+-- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
+-- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_nopk"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (c);
+-- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+-- Tests for partitioned table
+-- set PUBLISH_VIA_PARTITION_ROOT to false and test row filter for partitioned
+-- table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
+-- fail - cannot use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (a);
+ERROR:  cannot use publication column list for relation "rf_tbl_abcd_part_pk"
+DETAIL:  column list cannot be used for a partitioned table when publish_via_partition_root is false.
+-- ok - can use row filter for partition
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk_1 (a);
+-- ok - "a" is a PK col
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+-- set PUBLISH_VIA_PARTITION_ROOT to true and test row filter for partitioned
+-- table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=1);
+-- ok - can use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (a);
+-- ok - "a" is a PK col
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+-- fail - cannot set PUBLISH_VIA_PARTITION_ROOT to false if any row filter is
+-- used for partitioned table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
+ERROR:  cannot set publish_via_partition_root = false for publication "testpub6"
+DETAIL:  The publication contains a column filter for a partitioned table "rf_tbl_abcd_part_pk" which is not allowed when publish_via_partition_root is false.
+-- Now change the root filter to use a column "b"
+-- (which is not in the replica identity)
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk_1 (b);
+-- ok - we don't have row filter for partitioned table.
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
+-- fail - "b" is not in REPLICA IDENTITY INDEX
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_part_pk_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+-- set PUBLISH_VIA_PARTITION_ROOT to true
+-- can use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=1);
+-- ok - can use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (b);
+-- fail - "b" is not in REPLICA IDENTITY INDEX
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_part_pk_1"
+DETAIL:  Column filter used by the publication does not cover the replica identity.
+DROP PUBLICATION testpub6;
+DROP TABLE rf_tbl_abcd_pk;
+DROP TABLE rf_tbl_abcd_nopk;
+DROP TABLE rf_tbl_abcd_part_pk;
+-- ======================================================
 -- Test cache invalidation FOR ALL TABLES publication
 SET client_min_messages = 'ERROR';
 CREATE TABLE testpub_tbl4(a int);
@@ -1124,6 +1490,15 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_tes
 Tables from schemas:
     "pub_test1"
 
index c135a601a30..2203dc238d2 100644
@@ -381,6 +381,292 @@ DROP TABLE rf_tbl_abcd_nopk;
 
+-- fail - duplicate tables are not allowed if that table has any column filters
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1 (a), testpub_tbl1 WITH (publish = 'insert');
+CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1, testpub_tbl1 (a) WITH (publish = 'insert');
+RESET client_min_messages;
+
+-- test for column filters
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
+CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
+RESET client_min_messages;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
+	d int generated always as (a + length(b)) stored);
+-- error: column "x" does not exist
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
+-- error: replica identity "a" not included in the column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);
+UPDATE testpub_tbl5 SET a = 1;
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+-- error: generated column "d" can't be in list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
+-- error: system attributes "ctid" not allowed in column filter
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, ctid);
+-- ok
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
+-- ok: for insert-only publication, the filter is arbitrary
+ALTER PUBLICATION testpub_fortable_insert ADD TABLE testpub_tbl5 (b, c);
+
+/* not all replica identities are good enough */
+CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
+ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
+-- error: replica identity (b,c) is covered by column filter (a, c)
+UPDATE testpub_tbl5 SET a = 1;
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+
+-- error: change the replica identity to "b", and then filter (a, c) fails
+ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
+-- error: replica identity (b,c) is not covered by column filter (a,c)
+UPDATE testpub_tbl5 SET a = 1;
+
+/* But if upd/del are not published, it works OK */
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
+RESET client_min_messages;
+ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
+\dRp+ testpub_table_ins
+
+-- with REPLICA IDENTITY FULL, column filters are not allowed
+CREATE TABLE testpub_tbl6 (a int, b text, c text);
+ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
+
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);
+UPDATE testpub_tbl6 SET a = 1;
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl6;
+
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
+UPDATE testpub_tbl6 SET a = 1;
+
+ALTER PUBLICATION testpub_fortable ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);
+UPDATE testpub_tbl6 SET a = 1;
+
+-- make sure changing the column filter is updated in SET TABLE
+CREATE TABLE testpub_tbl7 (a int primary key, b text, c text);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+-- ok: we'll skip this table
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b);
+\d+ testpub_tbl7
+-- ok: update the column filter
+ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c);
+\d+ testpub_tbl7
+
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 0);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 1);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (b);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+
+-- ok: column filter covers both "a" and "b"
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
+RESET client_min_messages;
+
+-- ok: the same thing, but try plain ADD TABLE
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+UPDATE testpub_tbl8 SET a = 1;
+
+-- failure: column filter does not cover replica identity for the second partition
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+UPDATE testpub_tbl8 SET a = 1;
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+
+-- failure: one of the partitions has REPLICA IDENTITY FULL
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+UPDATE testpub_tbl8 SET a = 1;
+ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+
+-- add table and then try changing replica identity
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+
+-- failure: replica identity full can't be used with a column filter
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
+UPDATE testpub_tbl8 SET a = 1;
+
+-- failure: replica identity has to be covered by the column filter
+ALTER TABLE testpub_tbl8_1 DROP CONSTRAINT testpub_tbl8_1_pkey;
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+UPDATE testpub_tbl8 SET a = 1;
+
+DROP TABLE testpub_tbl8;
+
+-- column filter for partitioned tables has to cover replica identities for
+-- all child relations
+CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
+ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+-- first partition has replica identity "a"
+CREATE TABLE testpub_tbl8_0 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
+-- second partition has replica identity "b"
+CREATE TABLE testpub_tbl8_1 (a int, b text, c text);
+ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
+ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
+
+-- ok: attaching first partition works, because (a) is in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_0 FOR VALUES WITH (modulus 2, remainder 0);
+-- failure: second partition has replica identity (c), which si not in column filter
+ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_1 FOR VALUES WITH (modulus 2, remainder 1);
+UPDATE testpub_tbl8 SET a = 1;
+
+-- failure: changing replica identity to FULL for partition fails, because
+-- of the column filter on the parent
+ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY FULL;
+UPDATE testpub_tbl8 SET a = 1;
+
+DROP TABLE testpub_tbl5, testpub_tbl6, testpub_tbl7, testpub_tbl8, testpub_tbl8_1;
+DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_filter;
+-- ======================================================
+
+-- Test combination of column and row filter
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_both_filters;
+RESET client_min_messages;
+CREATE TABLE testpub_tbl_both_filters (a int, b int, c int, PRIMARY KEY (a,c));
+ALTER TABLE testpub_tbl_both_filters REPLICA IDENTITY USING INDEX testpub_tbl_both_filters_pkey;
+ALTER PUBLICATION testpub_both_filters ADD TABLE testpub_tbl_both_filters (a,c) WHERE (c != 1);
+\dRp+ testpub_both_filters
+\d+ testpub_tbl_both_filters
+
+DROP TABLE testpub_tbl_both_filters;
+DROP PUBLICATION testpub_both_filters;
+-- ======================================================
+
+-- More column filter tests for validating column references
+CREATE TABLE rf_tbl_abcd_nopk(a int, b int, c int, d int);
+CREATE TABLE rf_tbl_abcd_pk(a int, b int, c int, d int, PRIMARY KEY(a,b));
+CREATE TABLE rf_tbl_abcd_part_pk (a int PRIMARY KEY, b int) PARTITION by RANGE (a);
+CREATE TABLE rf_tbl_abcd_part_pk_1 (b int, a int PRIMARY KEY);
+ALTER TABLE rf_tbl_abcd_part_pk ATTACH PARTITION rf_tbl_abcd_part_pk_1 FOR VALUES FROM (1) TO (10);
+
+-- Case 1. REPLICA IDENTITY DEFAULT (means use primary key or nothing)
+
+-- 1a. REPLICA IDENTITY is DEFAULT and table has a PK.
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub6 FOR TABLE rf_tbl_abcd_pk (a, b);
+RESET client_min_messages;
+-- ok - (a,b) coverts all PK cols
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c);
+-- ok - (a,b,c) coverts all PK cols
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
+-- fail - "b" is missing from the column filter
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (b);
+-- fail - "a" is missing from the column filter
+UPDATE rf_tbl_abcd_pk SET a = 1;
+
+-- 1b. REPLICA IDENTITY is DEFAULT and table has no PK
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
+-- ok - there's no replica identity, so any column filter works
+-- note: it fails anyway, just a bit later because UPDATE requires RI
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+
+-- Case 2. REPLICA IDENTITY FULL
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY FULL;
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY FULL;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
+-- fail - with REPLICA IDENTITY FULL no column filter is allowed
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a, b, c, d);
+-- fail - with REPLICA IDENTITY FULL no column filter is allowed
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+
+-- Case 3. REPLICA IDENTITY NOTHING
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY NOTHING;
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
+-- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- it still fails later because without RI we can't replicate updates
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c, d);
+-- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- it still fails later because without RI we can't replicate updates
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (d);
+-- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- it still fails later because without RI we can't replicate updates
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+
+-- Case 4. REPLICA IDENTITY INDEX
+ALTER TABLE rf_tbl_abcd_pk ALTER COLUMN c SET NOT NULL;
+CREATE UNIQUE INDEX idx_abcd_pk_c ON rf_tbl_abcd_pk(c);
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY USING INDEX idx_abcd_pk_c;
+ALTER TABLE rf_tbl_abcd_nopk ALTER COLUMN c SET NOT NULL;
+CREATE UNIQUE INDEX idx_abcd_nopk_c ON rf_tbl_abcd_nopk(c);
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY USING INDEX idx_abcd_nopk_c;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
+-- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
+-- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
+-- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (c);
+-- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+
+-- Tests for partitioned table
+
+-- set PUBLISH_VIA_PARTITION_ROOT to false and test row filter for partitioned
+-- table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
+-- fail - cannot use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (a);
+-- ok - can use row filter for partition
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk_1 (a);
+-- ok - "a" is a PK col
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+-- set PUBLISH_VIA_PARTITION_ROOT to true and test row filter for partitioned
+-- table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=1);
+-- ok - can use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (a);
+-- ok - "a" is a PK col
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+-- fail - cannot set PUBLISH_VIA_PARTITION_ROOT to false if any row filter is
+-- used for partitioned table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
+-- Now change the root filter to use a column "b"
+-- (which is not in the replica identity)
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk_1 (b);
+-- ok - we don't have row filter for partitioned table.
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
+-- fail - "b" is not in REPLICA IDENTITY INDEX
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+-- set PUBLISH_VIA_PARTITION_ROOT to true
+-- can use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=1);
+-- ok - can use row filter for partitioned table
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (b);
+-- fail - "b" is not in REPLICA IDENTITY INDEX
+UPDATE rf_tbl_abcd_part_pk SET a = 1;
+
+DROP PUBLICATION testpub6;
+DROP TABLE rf_tbl_abcd_pk;
+DROP TABLE rf_tbl_abcd_nopk;
+DROP TABLE rf_tbl_abcd_part_pk;
+-- ======================================================
+
 -- Test cache invalidation FOR ALL TABLES publication
 SET client_min_messages = 'ERROR';
 CREATE TABLE testpub_tbl4(a int);
@@ -622,6 +908,10 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA non_existent_schem
 ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_test1;
 \dRp+ testpub1_forschema

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 9 Mar 2022 10:20:57 +0100

> approach (i.e. it should fail the same way as for row filter).

The first two patches (additional testing for row filtering feature) 
look okay to me.

Attached is a fixup patch for your main feature patch (the third one).

It's a bit of code and documentation cleanup, but mainly I removed the 
term "column filter" from the patch.  Half the code was using "column 
list" or similar and half the code "column filter", which was confusing. 
  Also, there seemed to be a bit of copy-and-pasting from row-filter 
code going on, with some code comments not quite sensible, so I rewrote 
some of them.  Also some code used "rf" and "cf" symbols which were a 
bit hard to tell apart.  A few more letters can increase readability.

Note in publicationcmds.c OpenTableList() the wrong if condition was used.

I'm still confused about the intended replica identity handling.  This 
patch still checks whether the column list contains the replica identity 
at DDL time.  And then it also checks at execution time.  I thought the 
latest understanding was that the DDL-time checking would be removed.  I 
think it's basically useless now, since as the test cases show, you can 
subvert those checks by altering the replica identity later.

From d0e9df4674389cda9f891f5678f476d35095c618 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 8 Mar 2022 16:23:01 +0100
Subject: [PATCH] fixup! Allow specifying column filters for logical
 replication

---
 doc/src/sgml/catalogs.sgml                  |   4 +-
 doc/src/sgml/protocol.sgml                  |   5 +-
 doc/src/sgml/ref/alter_publication.sgml     |   4 +
 src/backend/catalog/pg_publication.c        |  20 ++-
 src/backend/commands/publicationcmds.c      |  80 ++++++------
 src/backend/executor/execReplication.c      |   8 +-
 src/backend/replication/logical/proto.c     |  21 +--
 src/backend/replication/logical/tablesync.c |  16 +--
 src/backend/replication/pgoutput/pgoutput.c |   2 +-
 src/backend/utils/cache/relcache.c          |  25 ++--
 src/bin/psql/describe.c                     |   2 +-
 src/include/catalog/pg_publication.h        |   7 +-
 src/include/commands/publicationcmds.h      |   4 +-
 src/test/regress/expected/publication.out   | 134 ++++++++++----------
 src/test/regress/sql/publication.sql        |  90 ++++++-------
 src/test/subscription/t/029_column_list.pl  |  50 ++++----
 16 files changed, 235 insertions(+), 237 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2b61f42b71..c043da37ae 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -4392,7 +4392,7 @@ <title><structname>pg_index</structname> Columns</title>
       </para>
       <para>
        This is an array of <structfield>indnatts</structfield> values that
-       indicate which table columns this index indexes.  For example a value
+       indicate which table columns this index indexes.  For example, a value
        of <literal>1 3</literal> would mean that the first and the third table
        columns make up the index entries.  Key columns come before non-key
        (included) columns.  A zero in this array indicates that the
@@ -6271,7 +6271,7 @@ <title><structname>pg_publication_namespace</structname> Columns</title>
       </para>
       <para>
        This is an array of values that indicates which table columns are
-       part of the publication.  For example a value of <literal>1 3</literal>
+       part of the publication.  For example, a value of <literal>1 3</literal>
        would mean that the first and the third table columns are published.
        A null value indicates that all columns are published.
       </para></entry>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 496593201b..6f4d76ef7f 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -7005,9 +7005,8 @@ <title>Logical Replication Message Formats</title>
 </listitem>
 </varlistentry>
 </variablelist>
-        Next, the following message part appears for each column (except
-        generated columns and other columns that don't appear in the column
-        filter list, for tables that have one):
+        Next, the following message part appears for each column included in
+        the publication (except generated columns):
 <variablelist>
 <varlistentry>
 <term>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index aa6827c977..470d50a244 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -119,10 +119,14 @@ <title>Parameters</title>
       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.
+     </para>
 
+     <para>
       Optionally, a column list can be specified.  See <xref
       linkend="sql-createpublication"/> for details.
+     </para>
 
+     <para>
       If the optional <literal>WHERE</literal> clause is specified, rows for
       which the <replaceable class="parameter">expression</replaceable>
       evaluates to false or null will not be published. Note that parentheses
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 4dab96265f..3275a7c8b9 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -333,7 +333,6 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	Publication *pub = GetPublication(pubid);
 	AttrNumber *attarray;
 	int			natts = 0;
-	int			i;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
@@ -381,7 +380,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
 
-	/* Add column filter, if available */
+	/* Add column list, if available */
 	if (pri->columns)
 	{
 		int2vector *prattrs;
@@ -408,7 +407,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	ObjectAddressSet(myself, PublicationRelRelationId, pubreloid);
 
 	/* Add dependency on the columns, if any are listed */
-	for (i = 0; i < natts; i++)
+	for (int i = 0; i < natts; i++)
 	{
 		ObjectAddressSubSet(referenced, RelationRelationId, relid, attarray[i]);
 		recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
@@ -461,7 +460,6 @@ publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
 	bool		nulls[Natts_pg_publication_rel];
 	bool		replaces[Natts_pg_publication_rel];
 	Datum		values[Natts_pg_publication_rel];
-	int			i;
 
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
@@ -480,14 +478,14 @@ publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
 	}
 	else
 	{
-		ObjectAddress 	myself,
+		ObjectAddress	myself,
 						referenced;
 		int2vector	   *prattrs;
 		Form_pg_publication_rel	pubrel;
 
 		publication_translate_columns(targetrel, columns, &natts, &attarray);
 
-		/* XXX "pub" is leaked here */
+		/* XXX "pub" is leaked here ??? */
 
 		prattrs = buildint2vector(attarray, natts);
 		values[Anum_pg_publication_rel_prattrs - 1] = PointerGetDatum(prattrs);
@@ -496,7 +494,7 @@ publication_set_table_columns(Relation pubrel, HeapTuple pubreltup,
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(pubreltup);
 		ObjectAddressSet(myself, PublicationRelRelationId, pubrel->oid);
 
-		for (i = 0; i < natts; i++)
+		for (int i = 0; i < natts; i++)
 		{
 			ObjectAddressSubSet(referenced, RelationRelationId,
 								RelationGetRelid(targetrel), attarray[i]);
@@ -713,11 +711,10 @@ GetRelationColumnPartialPublications(Oid relid)
 {
 	CatCList   *pubrellist;
 	List	   *pubs = NIL;
-	int			i;
 
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		bool		isnull;
@@ -727,7 +724,7 @@ GetRelationColumnPartialPublications(Oid relid)
 							   Anum_pg_publication_rel_prattrs,
 							   &isnull);
 
-		/* no column filter for this publications/relation */
+		/* no column list for this publications/relation */
 		if (isnull)
 			continue;
 
@@ -756,7 +753,6 @@ GetRelationColumnListInPublication(Oid relid, Oid pubid)
 	int			nelems;
 	int16	   *elems;
 	List	   *attnos = NIL;
-	int			i;
 
 	tup = SearchSysCache2(PUBLICATIONRELMAP,
 						  ObjectIdGetDatum(relid),
@@ -774,7 +770,7 @@ GetRelationColumnListInPublication(Oid relid, Oid pubid)
 	nelems = ARR_DIMS(arr)[0];
 	elems = (int16 *) ARR_DATA_PTR(arr);
 
-	for (i = 0; i < nelems; i++)
+	for (int i = 0; i < nelems; i++)
 		attnos = lappend_oid(attnos, elems[i]);
 
 	ReleaseSysCache(tup);
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index fa1462ae54..b32ec27555 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -296,7 +296,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  * Returns true if any invalid column is found.
  */
 bool
-contain_invalid_rfcolumn(Oid pubid, Relation relation, List *ancestors,
+pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 						 bool pubviaroot)
 {
 	HeapTuple	rftuple;
@@ -368,28 +368,28 @@ contain_invalid_rfcolumn(Oid pubid, Relation relation, List *ancestors,
 }
 
 /*
- * Check if all columns referenced in the column filter are part of the
+ * Check if all columns referenced in the column list are part of the
  * REPLICA IDENTITY index or not.
  *
  * Returns true if any invalid column is found.
  */
 bool
-contain_invalid_cfcolumn(Oid pubid, Relation relation, List *ancestors,
+pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 						 bool pubviaroot)
 {
-	HeapTuple	cftuple;
+	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
 	Oid			publish_as_relid = RelationGetRelid(relation);
 	bool		result = false;
-	Datum		cfdatum;
-	bool		cfisnull;
+	Datum		datum;
+	bool		isnull;
 
 	/*
 	 * For a partition, if pubviaroot is true, find the topmost ancestor that
-	 * is published via this publication as we need to use its column filter
+	 * is published via this publication as we need to use its column list
 	 * for the changes.
 	 *
-	 * Note that even though the column filter used is for an ancestor, the
+	 * Note that even though the column list used is for an ancestor, the
 	 * REPLICA IDENTITY used will be for the actual child table.
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
@@ -400,24 +400,24 @@ contain_invalid_cfcolumn(Oid pubid, Relation relation, List *ancestors,
 			publish_as_relid = relid;
 	}
 
-	cftuple = SearchSysCache2(PUBLICATIONRELMAP,
+	tuple = SearchSysCache2(PUBLICATIONRELMAP,
 							  ObjectIdGetDatum(publish_as_relid),
 							  ObjectIdGetDatum(pubid));
 
-	if (!HeapTupleIsValid(cftuple))
+	if (!HeapTupleIsValid(tuple))
 		return false;
 
-	cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+	datum = SysCacheGetAttr(PUBLICATIONRELMAP, tuple,
 							  Anum_pg_publication_rel_prattrs,
-							  &cfisnull);
+							  &isnull);
 
-	if (!cfisnull)
+	if (!isnull)
 	{
 		int	x;
 		Bitmapset  *idattrs;
 		Bitmapset  *columns = NULL;
 
-		/* With REPLICA IDENTITY FULL, no column filter is allowed. */
+		/* With REPLICA IDENTITY FULL, no column list is allowed. */
 		if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 			result = true;
 
@@ -426,7 +426,7 @@ contain_invalid_cfcolumn(Oid pubid, Relation relation, List *ancestors,
 			int			nelems;
 			int16	   *elems;
 
-			arr = DatumGetArrayTypeP(cfdatum);
+			arr = DatumGetArrayTypeP(datum);
 			nelems = ARR_DIMS(arr)[0];
 			elems = (int16 *) ARR_DATA_PTR(arr);
 
@@ -441,7 +441,7 @@ contain_invalid_cfcolumn(Oid pubid, Relation relation, List *ancestors,
 
 		/*
 		 * Attnums in the bitmap returned by RelationGetIndexAttrBitmap are
-		 * offset (to handle system columns the usual way), while column filter
+		 * offset (to handle system columns the usual way), while column list
 		 * does not use offset, so we can't do bms_is_subset(). Instead, we have
 		 * to loop over the idattrs and check all of them are in the filter.
 		 */
@@ -479,7 +479,7 @@ contain_invalid_cfcolumn(Oid pubid, Relation relation, List *ancestors,
 		bms_free(columns);
 	}
 
-	ReleaseSysCache(cftuple);
+	ReleaseSysCache(tuple);
 
 	return result;
 }
@@ -733,7 +733,7 @@ TransformPubWhereClauses(List *tables, const char *queryString,
  * XXX The name is a bit misleading, because we don't really transform
  * anything here - we merely check the column list is compatible with the
  * definition of the publication (with publish_via_partition_root=false)
- * we only allow filters on the leaf relations. So maybe rename it?
+ * we only allow column lists on the leaf relations. So maybe rename it?
  */
 static void
 TransformPubColumnList(List *tables, const char *queryString,
@@ -750,7 +750,7 @@ TransformPubColumnList(List *tables, const char *queryString,
 
 		/*
 		 * If the publication doesn't publish changes via the root partitioned
-		 * table, the partition's column filter will be used. So disallow using
+		 * table, the partition's column list will be used. So disallow using
 		 * the column list on partitioned table in this case.
 		 */
 		if (!pubviaroot &&
@@ -988,8 +988,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 
 	/*
 	 * If the publication doesn't publish changes via the root partitioned
-	 * table, the partition's row and column filter will be used. So disallow
-	 * using WHERE clause and column filters on partitioned table in this case.
+	 * table, the partition's row filter and column list will be used. So disallow
+	 * using WHERE clause and column lists on partitioned table in this case.
 	 */
 	if (!pubform->puballtables && publish_via_partition_root_given &&
 		!publish_via_partition_root)
@@ -997,7 +997,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		/*
 		 * Lock the publication so nobody else can do anything with it. This
 		 * prevents concurrent alter to add partitioned table(s) with WHERE
-		 * clause(s) and/or column filters which we don't allow when not
+		 * clause(s) and/or column lists which we don't allow when not
 		 * publishing via root.
 		 */
 		LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
@@ -1010,7 +1010,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		{
 			HeapTuple	rftuple;
 			Oid			relid = lfirst_oid(lc);
-			bool		has_column_filter;
+			bool		has_column_list;
 			bool		has_row_filter;
 
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1020,11 +1020,11 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 			has_row_filter
 				= !heap_attisnull(rftuple, Anum_pg_publication_rel_prqual, NULL);
 
-			has_column_filter
+			has_column_list
 				= !heap_attisnull(rftuple, Anum_pg_publication_rel_prattrs, NULL);
 
 			if (HeapTupleIsValid(rftuple) &&
-				(has_row_filter || has_column_filter))
+				(has_row_filter || has_column_list))
 			{
 				HeapTuple	tuple;
 
@@ -1046,13 +1046,13 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 										   "publish_via_partition_root")));
 
 					if ((relform->relkind == RELKIND_PARTITIONED_TABLE) &&
-						has_column_filter)
+						has_column_list)
 						ereport(ERROR,
 								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 								 errmsg("cannot set %s for publication \"%s\"",
 										"publish_via_partition_root = false",
 										stmt->pubname),
-								 errdetail("The publication contains a column filter for a partitioned table \"%s\" "
+								 errdetail("The publication contains a column list for a partitioned table \"%s\" "
 										   "which is not allowed when %s is false.",
 										   NameStr(relform->relname),
 										   "publish_via_partition_root")));
@@ -1067,7 +1067,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 
 	/*
 	 * FIXME check pubactions vs. replica identity, to ensure the replica
-	 * identity is included in the column filter. Only do this for update
+	 * identity is included in the column list. Only do this for update
 	 * and delete publications. See check_publication_columns.
 	 *
 	 * XXX This is needed because publish_via_partition_root may change,
@@ -1261,7 +1261,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 
 			/*
 			 * See if the existing relation currently has a WHERE clause or a
-			 * column filter. We need to compare those too.
+			 * column list. We need to compare those too.
 			 */
 			if (HeapTupleIsValid(rftuple))
 			{
@@ -1313,7 +1313,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				newrelid = RelationGetRelid(newpubrel->relation);
 
 				/*
-				 * If the new publication has column filter, transform it to
+				 * If the new publication has column list, transform it to
 				 * a bitmap too.
 				 */
 				if (newpubrel->columns)
@@ -1710,7 +1710,7 @@ OpenTableList(List *tables)
 	List	   *rels = NIL;
 	ListCell   *lc;
 	List	   *relids_with_rf = NIL;
-	List	   *relids_with_cf = NIL;
+	List	   *relids_with_collist = NIL;
 
 	/*
 	 * Open, share-lock, and check all the explicitly-specified relations
@@ -1745,11 +1745,11 @@ OpenTableList(List *tables)
 						 errmsg("conflicting or redundant WHERE clauses for table \"%s\"",
 								RelationGetRelationName(rel))));
 
-			/* Disallow duplicate tables if there are any with column filters. */
-			if (t->columns || list_member_oid(relids_with_cf, myrelid))
+			/* Disallow duplicate tables if there are any with column lists. */
+			if (t->columns || list_member_oid(relids_with_collist, myrelid))
 				ereport(ERROR,
 						(errcode(ERRCODE_DUPLICATE_OBJECT),
-						 errmsg("conflicting or redundant column filters for table \"%s\"",
+						 errmsg("conflicting or redundant column lists for table \"%s\"",
 								RelationGetRelationName(rel))));
 
 			table_close(rel, ShareUpdateExclusiveLock);
@@ -1767,7 +1767,7 @@ OpenTableList(List *tables)
 			relids_with_rf = lappend_oid(relids_with_rf, myrelid);
 
 		if (t->columns)
-			relids_with_cf = lappend_oid(relids_with_cf, myrelid);
+			relids_with_collist = lappend_oid(relids_with_collist, myrelid);
 
 		/*
 		 * Add children of this rel, if requested, so that they too are added
@@ -1809,15 +1809,15 @@ OpenTableList(List *tables)
 										RelationGetRelationName(rel))));
 
 					/*
-					 * We don't allow to specify column filter for both parent
+					 * We don't allow to specify column list for both parent
 					 * and child table at the same time as it is not very
 					 * clear which one should be given preference.
 					 */
 					if (childrelid != myrelid &&
-						(t->columns || list_member_oid(relids_with_cf, childrelid)))
+						(t->columns || list_member_oid(relids_with_collist, childrelid)))
 						ereport(ERROR,
 								(errcode(ERRCODE_DUPLICATE_OBJECT),
-								 errmsg("conflicting or redundant column fiters for table \"%s\"",
+								 errmsg("conflicting or redundant column lists for table \"%s\"",
 										RelationGetRelationName(rel))));
 
 					continue;
@@ -1837,8 +1837,8 @@ OpenTableList(List *tables)
 				if (t->whereClause)
 					relids_with_rf = lappend_oid(relids_with_rf, childrelid);
 
-				if (t->whereClause)
-					relids_with_cf = lappend_oid(relids_with_cf, childrelid);
+				if (t->columns)
+					relids_with_collist = lappend_oid(relids_with_collist, childrelid);
 			}
 		}
 	}
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1d8d4af341..3e282ed99a 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -592,24 +592,24 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd)
 				 errmsg("cannot update table \"%s\"",
 						RelationGetRelationName(rel)),
 				 errdetail("Column used in the publication WHERE expression is not part of the replica identity.")));
-	else if (cmd == CMD_UPDATE && !pubdesc.cf_valid_for_update)
+	else if (cmd == CMD_UPDATE && !pubdesc.cols_valid_for_update)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
 				 errmsg("cannot update table \"%s\"",
 						RelationGetRelationName(rel)),
-				 errdetail("Column filter used by the publication does not cover the replica identity.")));
+				 errdetail("Column list used by the publication does not cover the replica identity.")));
 	else if (cmd == CMD_DELETE && !pubdesc.rf_valid_for_delete)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
 				 errmsg("cannot delete from table \"%s\"",
 						RelationGetRelationName(rel)),
 				 errdetail("Column used in the publication WHERE expression is not part of the replica identity.")));
-	else if (cmd == CMD_DELETE && !pubdesc.cf_valid_for_delete)
+	else if (cmd == CMD_DELETE && !pubdesc.cols_valid_for_delete)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
 				 errmsg("cannot delete from table \"%s\"",
 						RelationGetRelationName(rel)),
-				 errdetail("Column filter used by the publication does not cover the replica identity.")));
+				 errdetail("Column list used by the publication does not cover the replica identity.")));
 
 	/* If relation has replica identity we are always good. */
 	if (OidIsValid(RelationGetReplicaIndex(rel)))
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index 1e8785ff9a..816d461acd 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -40,8 +40,12 @@ static void logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple);
 static void logicalrep_write_namespace(StringInfo out, Oid nspid);
 static const char *logicalrep_read_namespace(StringInfo in);
 
-#define	ColumnInFilter(columns, attnum) \
-		(((columns) == NULL) || (bms_is_member((attnum), (columns))))
+
+static bool
+column_in_set(int attnum, Bitmapset *columns)
+{
+	return (columns == NULL || bms_is_member(attnum, columns));
+}

 /*
@@ -774,9 +778,9 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
-		/* skip columns not included in the column filter */
-		if (!ColumnInFilter(columns, att->attnum))
+		if (!column_in_set(att->attnum, columns))
 			continue;
+
 		nliveatts++;
 	}
 	pq_sendint16(out, nliveatts);
@@ -795,8 +799,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
-		/* skip columns not included in the column filter */
-		if (!ColumnInFilter(columns, att->attnum))
+		if (!column_in_set(att->attnum, columns))
 			continue;
 
 		if (isnull[i])
@@ -938,8 +941,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
-		/* skip columns not included in the column filter */
-		if (!ColumnInFilter(columns, att->attnum))
+		if (!column_in_set(att->attnum, columns))
 			continue;
 
 		nliveatts++;
@@ -960,8 +962,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *columns)
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
-		/* skip columns not included in the column filter */
-		if (!ColumnInFilter(columns, att->attnum))
+		if (!column_in_set(att->attnum, columns))
 			continue;
 
 		/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 42708dcf82..d4d504fe02 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -754,13 +754,13 @@ fetch_remote_table_info(char *nspname, char *relname,

 	/*
-	 * Get relation's column filter expressions.
+	 * Get column lists for each relation.
 	 *
-	 * For initial synchronization, column filter can be ignored in following
+	 * For initial synchronization, column lists can be ignored in following
 	 * cases:
 	 *
 	 * 1) one of the subscribed publications for the table hasn't specified
-	 * any column filter
+	 * any column list
 	 *
 	 * 2) one of the subscribed publications has puballtables set to true
 	 *
@@ -788,7 +788,7 @@ fetch_remote_table_info(char *nspname, char *relname,
 
 		/*
 		 * First, check if any of the publications FOR ALL TABLES? If yes, we
-		 * should not use any column filter. It's enough to find a single such
+		 * should not use any column list. It's enough to find a single such
 		 * publication.
 		 *
 		 * XXX Maybe we could combine all three steps into a single query, but
@@ -823,7 +823,7 @@ fetch_remote_table_info(char *nspname, char *relname,
 		/*
 		 * If there's no FOR ALL TABLES publication, look for a FOR ALL TABLES
 		 * IN SCHEMA publication, with schema of the remote relation. The logic
-		 * is the same - such publications have no column filters.
+		 * is the same - such publications have no column lists.
 		 *
 		 * XXX Does this need any handling of partitions / publish_via_part_root?
 		 */
@@ -859,8 +859,8 @@ fetch_remote_table_info(char *nspname, char *relname,
 
 		/*
 		 * If we haven't found any FOR ALL TABLES [IN SCHEMA] publications for
-		 * the table, we have to look for the column filters set for relations.
-		 * First, we check if there's a publication with no column filter for
+		 * the table, we have to look for the column lists for relations.
+		 * First, we check if there's a publication with no column list for
 		 * the relation - which means all columns need to be replicated.
 		 */
 		if (!all_columns)
@@ -906,7 +906,7 @@ fetch_remote_table_info(char *nspname, char *relname,
 		}
 
 		/*
-		 * All that 
+		 * All that FIXME
 		 */
 		if (!all_columns)
 		{
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 07cdfc1d8c..b4203788b0 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1936,7 +1936,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 						if (isnull)
 						{
 							/*
-							 * If we see a publication with no column filter, it
+							 * If we see a publication with no column list, it
 							 * means we need to publish all columns, so reset the
 							 * list and ignore further ones.
 							 */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 82e595396e..a2da72f0d4 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5553,8 +5553,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		memset(pubdesc, 0, sizeof(PublicationDesc));
 		pubdesc->rf_valid_for_update = true;
 		pubdesc->rf_valid_for_delete = true;
-		pubdesc->cf_valid_for_update = true;
-		pubdesc->cf_valid_for_delete = true;
+		pubdesc->cols_valid_for_update = true;
+		pubdesc->cols_valid_for_delete = true;
 		return;
 	}
 
@@ -5567,8 +5567,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	memset(pubdesc, 0, sizeof(PublicationDesc));
 	pubdesc->rf_valid_for_update = true;
 	pubdesc->rf_valid_for_delete = true;
-	pubdesc->cf_valid_for_update = true;
-	pubdesc->cf_valid_for_delete = true;
+	pubdesc->cols_valid_for_update = true;
+	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
 	puboids = GetRelationPublications(relid);
@@ -5620,7 +5620,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		 */
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
-			contain_invalid_rfcolumn(pubid, relation, ancestors,
+			pub_rf_contains_invalid_column(pubid, relation, ancestors,
 									 pubform->pubviaroot))
 		{
 			if (pubform->pubupdate)
@@ -5630,21 +5630,20 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		}
 
 		/*
-		 * Check if all columns referenced in the column filter are part of
-		 * the REPLICA IDENTITY index or not.
+		 * Check if all columns are part of the REPLICA IDENTITY index or not.
 		 *
 		 * If the publication is FOR ALL TABLES then it means the table has no
-		 * column filters and we can skip the validation.
+		 * column list and we can skip the validation.
 		 */
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
-			contain_invalid_cfcolumn(pubid, relation, ancestors,
+			pub_collist_contains_invalid_column(pubid, relation, ancestors,
 									 pubform->pubviaroot))
 		{
 			if (pubform->pubupdate)
-				pubdesc->cf_valid_for_update = false;
+				pubdesc->cols_valid_for_update = false;
 			if (pubform->pubdelete)
-				pubdesc->cf_valid_for_delete = false;
+				pubdesc->cols_valid_for_delete = false;
 		}
 
 		ReleaseSysCache(tup);
@@ -5660,13 +5659,13 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			break;
 
 		/*
-		 * If we know everything is replicated and the column filter is invalid
+		 * If we know everything is replicated and the column list is invalid
 		 * for update and delete, there is no point to check for other
 		 * publications.
 		 */
 		if (pubdesc->pubactions.pubinsert && pubdesc->pubactions.pubupdate &&
 			pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
-			!pubdesc->cf_valid_for_update && !pubdesc->cf_valid_for_delete)
+			!pubdesc->cols_valid_for_update && !pubdesc->cols_valid_for_delete)
 			break;
 	}
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index fb18cb82d9..e462ccfb74 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2941,7 +2941,7 @@ describeOneTableDetails(const char *schemaname,
 				printfPQExpBuffer(&buf, "    \"%s\"",
 								  PQgetvalue(result, i, 0));
 
-				/* column filter (if any) */
+				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
 					appendPQExpBuffer(&buf, " (%s)",
 									  PQgetvalue(result, i, 2));
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index b58f85ede2..a06742a620 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -87,12 +87,11 @@ typedef struct PublicationDesc
 	bool		rf_valid_for_delete;
 
 	/*
-	 * true if the columns referenced in column filters which are used for UPDATE
-	 * or DELETE are part of the replica identity or the publication actions
+	 * true if the columns are part of the replica identity or the publication actions
 	 * do not include UPDATE or DELETE.
 	 */
-	bool		cf_valid_for_update;
-	bool		cf_valid_for_delete;
+	bool		cols_valid_for_update;
+	bool		cols_valid_for_delete;
 } PublicationDesc;
 
 typedef struct Publication
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 08d14ca724..ae87caf089 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -31,9 +31,9 @@ extern void RemovePublicationSchemaById(Oid psoid);
 extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
-extern bool contain_invalid_rfcolumn(Oid pubid, Relation relation,
+extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
 									 List *ancestors, bool pubviaroot);
-extern bool contain_invalid_cfcolumn(Oid pubid, Relation relation,
+extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
 									 List *ancestors, bool pubviaroot);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 152f19fb42..80202f84ff 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -679,14 +679,14 @@ DROP TABLE rf_tbl_abcd_pk;
 DROP TABLE rf_tbl_abcd_nopk;
 DROP TABLE rf_tbl_abcd_part_pk;
 -- ======================================================
--- fail - duplicate tables are not allowed if that table has any column filters
+-- fail - duplicate tables are not allowed if that table has any column lists
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1 (a), testpub_tbl1 WITH (publish = 'insert');
-ERROR:  conflicting or redundant column filters for table "testpub_tbl1"
+ERROR:  conflicting or redundant column lists for table "testpub_tbl1"
 CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1, testpub_tbl1 (a) WITH (publish = 'insert');
-ERROR:  conflicting or redundant column filters for table "testpub_tbl1"
+ERROR:  conflicting or redundant column lists for table "testpub_tbl1"
 RESET client_min_messages;
--- test for column filters
+-- test for column lists
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
@@ -696,16 +696,16 @@ CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
 -- error: column "x" does not exist
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
 ERROR:  column "x" of relation "testpub_tbl5" does not exist
--- error: replica identity "a" not included in the column filter
+-- error: replica identity "a" not included in the column list
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);
 UPDATE testpub_tbl5 SET a = 1;
 ERROR:  cannot update table "testpub_tbl5"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
 -- error: generated column "d" can't be in list
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
 ERROR:  cannot reference generated column "d" in publication column list
--- error: system attributes "ctid" not allowed in column filter
+-- error: system attributes "ctid" not allowed in column list
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, ctid);
 ERROR:  cannot reference system column "ctid" in publication column list
 -- ok
@@ -713,24 +713,24 @@ ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
 ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
 ERROR:  cannot drop column "c" because it is part of publication "testpub_fortable"
 HINT:  Specify CASCADE or use ALTER PUBLICATION to remove the column from the publication.
--- ok: for insert-only publication, the filter is arbitrary
+-- ok: for insert-only publication, the column list is arbitrary
 ALTER PUBLICATION testpub_fortable_insert ADD TABLE testpub_tbl5 (b, c);
 /* not all replica identities are good enough */
 CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
 ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
 ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
--- error: replica identity (b,c) is covered by column filter (a, c)
+-- error: replica identity (b,c) is covered by column list (a, c)
 UPDATE testpub_tbl5 SET a = 1;
 ERROR:  cannot update table "testpub_tbl5"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
--- error: change the replica identity to "b", and then filter (a, c) fails
+-- error: change the replica identity to "b", and then column list (a, c) fails
 ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
--- error: replica identity (b,c) is not covered by column filter (a,c)
+-- error: replica identity (b,c) is not covered by column list (a, c)
 UPDATE testpub_tbl5 SET a = 1;
 ERROR:  cannot update table "testpub_tbl5"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 /* But if upd/del are not published, it works OK */
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
@@ -744,21 +744,21 @@ ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
 Tables:
     "public.testpub_tbl5" (a)
 
--- with REPLICA IDENTITY FULL, column filters are not allowed
+-- with REPLICA IDENTITY FULL, column lists are not allowed
 CREATE TABLE testpub_tbl6 (a int, b text, c text);
 ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6 (a, b, c);
 UPDATE testpub_tbl6 SET a = 1;
 ERROR:  cannot update table "testpub_tbl6"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl6;
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl6; -- ok
 UPDATE testpub_tbl6 SET a = 1;
 ALTER PUBLICATION testpub_fortable ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);
 UPDATE testpub_tbl6 SET a = 1;
 ERROR:  cannot update table "testpub_tbl6"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
--- make sure changing the column filter is updated in SET TABLE
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- make sure changing the column list is updated in SET TABLE
 CREATE TABLE testpub_tbl7 (a int primary key, b text, c text);
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl7 (a, b);
 \d+ testpub_tbl7
@@ -787,7 +787,7 @@ Indexes:
 Publications:
     "testpub_fortable" (a, b)
 
--- ok: update the column filter
+-- ok: update the column list
 ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c);
 \d+ testpub_tbl7
                                 Table "public.testpub_tbl7"
@@ -801,7 +801,7 @@ Indexes:
 Publications:
     "testpub_fortable" (a, c)
 
--- column filter for partitioned tables has to cover replica identities for
+-- column list for partitioned tables has to cover replica identities for
 -- all child relations
 CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
 -- first partition has replica identity "a"
@@ -812,48 +812,48 @@ ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
 CREATE TABLE testpub_tbl8_1 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2, remainder 1);
 ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (b);
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
--- ok: column filter covers both "a" and "b"
+-- ok: column list covers both "a" and "b"
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
+CREATE PUBLICATION testpub_col_list FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
 RESET client_min_messages;
 -- ok: the same thing, but try plain ADD TABLE
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, b);
 UPDATE testpub_tbl8 SET a = 1;
--- failure: column filter does not cover replica identity for the second partition
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+-- failure: column list does not cover replica identity for the second partition
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, c);
 UPDATE testpub_tbl8 SET a = 1;
 ERROR:  cannot update table "testpub_tbl8_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+DETAIL:  Column list used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
 -- failure: one of the partitions has REPLICA IDENTITY FULL
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, c);
 UPDATE testpub_tbl8 SET a = 1;
 ERROR:  cannot update table "testpub_tbl8_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+DETAIL:  Column list used by the publication does not cover the replica identity.
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
 -- add table and then try changing replica identity
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
--- failure: replica identity full can't be used with a column filter
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, b);
+-- failure: replica identity full can't be used with a column list
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
 UPDATE testpub_tbl8 SET a = 1;
 ERROR:  cannot update table "testpub_tbl8_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
--- failure: replica identity has to be covered by the column filter
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- failure: replica identity has to be covered by the column list
 ALTER TABLE testpub_tbl8_1 DROP CONSTRAINT testpub_tbl8_1_pkey;
 ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
 UPDATE testpub_tbl8 SET a = 1;
 ERROR:  cannot update table "testpub_tbl8_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 DROP TABLE testpub_tbl8;
--- column filter for partitioned tables has to cover replica identities for
+-- column list for partitioned tables has to cover replica identities for
 -- all child relations
 CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, b);
 -- first partition has replica identity "a"
 CREATE TABLE testpub_tbl8_0 (a int, b text, c text);
 ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
@@ -862,23 +862,23 @@ ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY USING INDEX testpub_tbl8_0_pkey;
 CREATE TABLE testpub_tbl8_1 (a int, b text, c text);
 ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
--- ok: attaching first partition works, because (a) is in column filter
+-- ok: attaching first partition works, because (a) is in column list
 ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_0 FOR VALUES WITH (modulus 2, remainder 0);
--- failure: second partition has replica identity (c), which si not in column filter
+-- failure: second partition has replica identity (c), which si not in column list
 ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_1 FOR VALUES WITH (modulus 2, remainder 1);
 UPDATE testpub_tbl8 SET a = 1;
 ERROR:  cannot update table "testpub_tbl8_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 -- failure: changing replica identity to FULL for partition fails, because
--- of the column filter on the parent
+-- of the column list on the parent
 ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY FULL;
 UPDATE testpub_tbl8 SET a = 1;
 ERROR:  cannot update table "testpub_tbl8_0"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 DROP TABLE testpub_tbl5, testpub_tbl6, testpub_tbl7, testpub_tbl8, testpub_tbl8_1;
-DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_filter;
+DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_list;
 -- ======================================================
--- Test combination of column and row filter
+-- Test combination of column list and row filter
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_both_filters;
 RESET client_min_messages;
@@ -908,7 +908,7 @@ Publications:
 DROP TABLE testpub_tbl_both_filters;
 DROP PUBLICATION testpub_both_filters;
 -- ======================================================
--- More column filter tests for validating column references
+-- More column list tests for validating column references
 CREATE TABLE rf_tbl_abcd_nopk(a int, b int, c int, d int);
 CREATE TABLE rf_tbl_abcd_pk(a int, b int, c int, d int, PRIMARY KEY(a,b));
 CREATE TABLE rf_tbl_abcd_part_pk (a int PRIMARY KEY, b int) PARTITION by RANGE (a);
@@ -925,18 +925,18 @@ ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c);
 -- ok - (a,b,c) coverts all PK cols
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
--- fail - "b" is missing from the column filter
+-- fail - "b" is missing from the column list
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (b);
--- fail - "a" is missing from the column filter
+-- fail - "a" is missing from the column list
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 -- 1b. REPLICA IDENTITY is DEFAULT and table has no PK
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
--- ok - there's no replica identity, so any column filter works
+-- ok - there's no replica identity, so any column list works
 -- note: it fails anyway, just a bit later because UPDATE requires RI
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk" because it does not have a replica identity and publishes updates
@@ -945,32 +945,32 @@ HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
 ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY FULL;
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY FULL;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
--- fail - with REPLICA IDENTITY FULL no column filter is allowed
+-- fail - with REPLICA IDENTITY FULL no column list is allowed
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a, b, c, d);
--- fail - with REPLICA IDENTITY FULL no column filter is allowed
+-- fail - with REPLICA IDENTITY FULL no column list is allowed
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 -- Case 3. REPLICA IDENTITY NOTHING
 ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY NOTHING;
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
--- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- ok - REPLICA IDENTITY NOTHING means all column lists are valid
 -- it still fails later because without RI we can't replicate updates
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c, d);
--- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- ok - REPLICA IDENTITY NOTHING means all column lists are valid
 -- it still fails later because without RI we can't replicate updates
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (d);
--- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- ok - REPLICA IDENTITY NOTHING means all column lists are valid
 -- it still fails later because without RI we can't replicate updates
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk" because it does not have a replica identity and publishes updates
@@ -983,20 +983,20 @@ ALTER TABLE rf_tbl_abcd_nopk ALTER COLUMN c SET NOT NULL;
 CREATE UNIQUE INDEX idx_abcd_nopk_c ON rf_tbl_abcd_nopk(c);
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY USING INDEX idx_abcd_nopk_c;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
--- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+-- fail - column list "a" does not cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
--- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+-- ok - column list "c" does cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
--- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+-- fail - column list "a" does not cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (c);
--- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+-- ok - column list "c" does cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 -- Tests for partitioned table
 -- set PUBLISH_VIA_PARTITION_ROOT to false and test row filter for partitioned
@@ -1021,7 +1021,7 @@ UPDATE rf_tbl_abcd_part_pk SET a = 1;
 -- used for partitioned table
 ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
 ERROR:  cannot set publish_via_partition_root = false for publication "testpub6"
-DETAIL:  The publication contains a column filter for a partitioned table "rf_tbl_abcd_part_pk" which is not allowed when publish_via_partition_root is false.
+DETAIL:  The publication contains a column list for a partitioned table "rf_tbl_abcd_part_pk" which is not allowed when publish_via_partition_root is false.
 -- Now change the root filter to use a column "b"
 -- (which is not in the replica identity)
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk_1 (b);
@@ -1030,7 +1030,7 @@ ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
 -- fail - "b" is not in REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_part_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_part_pk_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 -- set PUBLISH_VIA_PARTITION_ROOT to true
 -- can use row filter for partitioned table
 ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=1);
@@ -1039,7 +1039,7 @@ ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk (b);
 -- fail - "b" is not in REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_part_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_part_pk_1"
-DETAIL:  Column filter used by the publication does not cover the replica identity.
+DETAIL:  Column list used by the publication does not cover the replica identity.
 DROP PUBLICATION testpub6;
 DROP TABLE rf_tbl_abcd_pk;
 DROP TABLE rf_tbl_abcd_nopk;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 2203dc238d..32a810b2d2 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -381,13 +381,13 @@ CREATE UNIQUE INDEX idx_abcd_nopk_c ON rf_tbl_abcd_nopk(c);
 DROP TABLE rf_tbl_abcd_part_pk;
 -- ======================================================
 
--- fail - duplicate tables are not allowed if that table has any column filters
+-- fail - duplicate tables are not allowed if that table has any column lists
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1 (a), testpub_tbl1 WITH (publish = 'insert');
 CREATE PUBLICATION testpub_dups FOR TABLE testpub_tbl1, testpub_tbl1 (a) WITH (publish = 'insert');
 RESET client_min_messages;
 
--- test for column filters
+-- test for column lists
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
@@ -396,32 +396,32 @@ CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
 	d int generated always as (a + length(b)) stored);
 -- error: column "x" does not exist
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
--- error: replica identity "a" not included in the column filter
+-- error: replica identity "a" not included in the column list
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);
 UPDATE testpub_tbl5 SET a = 1;
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
 -- error: generated column "d" can't be in list
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
--- error: system attributes "ctid" not allowed in column filter
+-- error: system attributes "ctid" not allowed in column list
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, ctid);
 -- ok
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
 ALTER TABLE testpub_tbl5 DROP COLUMN c;		-- no dice
--- ok: for insert-only publication, the filter is arbitrary
+-- ok: for insert-only publication, the column list is arbitrary
 ALTER PUBLICATION testpub_fortable_insert ADD TABLE testpub_tbl5 (b, c);
 
 /* not all replica identities are good enough */
 CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
 ALTER TABLE testpub_tbl5 ALTER b SET NOT NULL, ALTER c SET NOT NULL;
 ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
--- error: replica identity (b,c) is covered by column filter (a, c)
+-- error: replica identity (b,c) is covered by column list (a, c)
 UPDATE testpub_tbl5 SET a = 1;
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
 
--- error: change the replica identity to "b", and then filter (a, c) fails
+-- error: change the replica identity to "b", and then column list (a, c) fails
 ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);
--- error: replica identity (b,c) is not covered by column filter (a,c)
+-- error: replica identity (b,c) is not covered by column list (a, c)
 UPDATE testpub_tbl5 SET a = 1;
 
 /* But if upd/del are not published, it works OK */
@@ -431,7 +431,7 @@ CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate');
 ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a);		-- ok
 \dRp+ testpub_table_ins
 
--- with REPLICA IDENTITY FULL, column filters are not allowed
+-- with REPLICA IDENTITY FULL, column lists are not allowed
 CREATE TABLE testpub_tbl6 (a int, b text, c text);
 ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
 
@@ -445,18 +445,18 @@ CREATE TABLE testpub_tbl6 (a int, b text, c text);
 ALTER PUBLICATION testpub_fortable ALTER TABLE testpub_tbl6 SET COLUMNS (a, b, c);
 UPDATE testpub_tbl6 SET a = 1;
 
--- make sure changing the column filter is updated in SET TABLE
+-- make sure changing the column list is updated in SET TABLE
 CREATE TABLE testpub_tbl7 (a int primary key, b text, c text);
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl7 (a, b);
 \d+ testpub_tbl7
 -- ok: we'll skip this table
 ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b);
 \d+ testpub_tbl7
--- ok: update the column filter
+-- ok: update the column list
 ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c);
 \d+ testpub_tbl7
 
--- column filter for partitioned tables has to cover replica identities for
+-- column list for partitioned tables has to cover replica identities for
 -- all child relations
 CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
 -- first partition has replica identity "a"
@@ -468,37 +468,37 @@ CREATE TABLE testpub_tbl8_1 PARTITION OF testpub_tbl8 FOR VALUES WITH (modulus 2
 ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (b);
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
 
--- ok: column filter covers both "a" and "b"
+-- ok: column list covers both "a" and "b"
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
+CREATE PUBLICATION testpub_col_list FOR TABLE testpub_tbl8 (a, b) WITH (publish_via_partition_root = 'true');
 RESET client_min_messages;
 
 -- ok: the same thing, but try plain ADD TABLE
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, b);
 UPDATE testpub_tbl8 SET a = 1;
 
--- failure: column filter does not cover replica identity for the second partition
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+-- failure: column list does not cover replica identity for the second partition
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, c);
 UPDATE testpub_tbl8 SET a = 1;
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
 
 -- failure: one of the partitions has REPLICA IDENTITY FULL
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, c);
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, c);
 UPDATE testpub_tbl8 SET a = 1;
-ALTER PUBLICATION testpub_col_filter DROP TABLE testpub_tbl8;
+ALTER PUBLICATION testpub_col_list DROP TABLE testpub_tbl8;
 
 -- add table and then try changing replica identity
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, b);
 
--- failure: replica identity full can't be used with a column filter
+-- failure: replica identity full can't be used with a column list
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY FULL;
 UPDATE testpub_tbl8 SET a = 1;
 
--- failure: replica identity has to be covered by the column filter
+-- failure: replica identity has to be covered by the column list
 ALTER TABLE testpub_tbl8_1 DROP CONSTRAINT testpub_tbl8_1_pkey;
 ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
@@ -506,10 +506,10 @@ CREATE PUBLICATION testpub_col_filter FOR TABLE testpub_tbl8 (a, b) WITH (publis
 
 DROP TABLE testpub_tbl8;
 
--- column filter for partitioned tables has to cover replica identities for
+-- column list for partitioned tables has to cover replica identities for
 -- all child relations
 CREATE TABLE testpub_tbl8 (a int, b text, c text) PARTITION BY HASH (a);
-ALTER PUBLICATION testpub_col_filter ADD TABLE testpub_tbl8 (a, b);
+ALTER PUBLICATION testpub_col_list ADD TABLE testpub_tbl8 (a, b);
 -- first partition has replica identity "a"
 CREATE TABLE testpub_tbl8_0 (a int, b text, c text);
 ALTER TABLE testpub_tbl8_0 ADD PRIMARY KEY (a);
@@ -519,22 +519,22 @@ CREATE TABLE testpub_tbl8_1 (a int, b text, c text);
 ALTER TABLE testpub_tbl8_1 ADD PRIMARY KEY (c);
 ALTER TABLE testpub_tbl8_1 REPLICA IDENTITY USING INDEX testpub_tbl8_1_pkey;
 
--- ok: attaching first partition works, because (a) is in column filter
+-- ok: attaching first partition works, because (a) is in column list
 ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_0 FOR VALUES WITH (modulus 2, remainder 0);
--- failure: second partition has replica identity (c), which si not in column filter
+-- failure: second partition has replica identity (c), which si not in column list
 ALTER TABLE testpub_tbl8 ATTACH PARTITION testpub_tbl8_1 FOR VALUES WITH (modulus 2, remainder 1);
 UPDATE testpub_tbl8 SET a = 1;
 
 -- failure: changing replica identity to FULL for partition fails, because
--- of the column filter on the parent
+-- of the column list on the parent
 ALTER TABLE testpub_tbl8_0 REPLICA IDENTITY FULL;
 UPDATE testpub_tbl8 SET a = 1;
 
 DROP TABLE testpub_tbl5, testpub_tbl6, testpub_tbl7, testpub_tbl8, testpub_tbl8_1;
-DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_filter;
+DROP PUBLICATION testpub_table_ins, testpub_fortable, testpub_fortable_insert, testpub_col_list;
 -- ======================================================
 
--- Test combination of column and row filter
+-- Test combination of column list and row filter
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_both_filters;
 RESET client_min_messages;
@@ -548,7 +548,7 @@ CREATE TABLE testpub_tbl_both_filters (a int, b int, c int, PRIMARY KEY (a,c));
 DROP PUBLICATION testpub_both_filters;
 -- ======================================================
 
--- More column filter tests for validating column references
+-- More column list tests for validating column references
 CREATE TABLE rf_tbl_abcd_nopk(a int, b int, c int, d int);
 CREATE TABLE rf_tbl_abcd_pk(a int, b int, c int, d int, PRIMARY KEY(a,b));
 CREATE TABLE rf_tbl_abcd_part_pk (a int PRIMARY KEY, b int) PARTITION by RANGE (a);
@@ -567,15 +567,15 @@ CREATE PUBLICATION testpub6 FOR TABLE rf_tbl_abcd_pk (a, b);
 -- ok - (a,b,c) coverts all PK cols
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
--- fail - "b" is missing from the column filter
+-- fail - "b" is missing from the column list
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (b);
--- fail - "a" is missing from the column filter
+-- fail - "a" is missing from the column list
 UPDATE rf_tbl_abcd_pk SET a = 1;
 
 -- 1b. REPLICA IDENTITY is DEFAULT and table has no PK
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
--- ok - there's no replica identity, so any column filter works
+-- ok - there's no replica identity, so any column list works
 -- note: it fails anyway, just a bit later because UPDATE requires RI
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 
@@ -583,25 +583,25 @@ CREATE PUBLICATION testpub6 FOR TABLE rf_tbl_abcd_pk (a, b);
 ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY FULL;
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY FULL;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
--- fail - with REPLICA IDENTITY FULL no column filter is allowed
+-- fail - with REPLICA IDENTITY FULL no column list is allowed
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a, b, c, d);
--- fail - with REPLICA IDENTITY FULL no column filter is allowed
+-- fail - with REPLICA IDENTITY FULL no column list is allowed
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 
 -- Case 3. REPLICA IDENTITY NOTHING
 ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY NOTHING;
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
--- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- ok - REPLICA IDENTITY NOTHING means all column lists are valid
 -- it still fails later because without RI we can't replicate updates
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a, b, c, d);
--- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- ok - REPLICA IDENTITY NOTHING means all column lists are valid
 -- it still fails later because without RI we can't replicate updates
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (d);
--- ok - REPLICA IDENTITY NOTHING means all column filters are valid
+-- ok - REPLICA IDENTITY NOTHING means all column lists are valid
 -- it still fails later because without RI we can't replicate updates
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 
@@ -613,16 +613,16 @@ CREATE UNIQUE INDEX idx_abcd_pk_c ON rf_tbl_abcd_pk(c);
 CREATE UNIQUE INDEX idx_abcd_nopk_c ON rf_tbl_abcd_nopk(c);
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY USING INDEX idx_abcd_nopk_c;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (a);
--- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+-- fail - column list "a" does not cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk (c);
--- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+-- ok - column list "c" does cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (a);
--- fail - column filter "a" does not cover the REPLICA IDENTITY INDEX on "c"
+-- fail - column list "a" does not cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk (c);
--- ok - column filter "c" does cover the REPLICA IDENTITY INDEX on "c"
+-- ok - column list "c" does cover the REPLICA IDENTITY INDEX on "c"
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 
 -- Tests for partitioned table
diff --git a/src/test/subscription/t/029_column_list.pl b/src/test/subscription/t/029_column_list.pl
index ec2c8a789a..e5ab5d5731 100644
--- a/src/test/subscription/t/029_column_list.pl
+++ b/src/test/subscription/t/029_column_list.pl
@@ -5,7 +5,7 @@
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 26;
+use Test::More;
 
 # setup
 
@@ -290,8 +290,8 @@ sub wait_for_subscription_sync
 2|22|2222),
    'overlapping publications with overlapping column lists');
 
-# and finally, set the column filter to ALL for one of the publications,
-# which means replicating all columns (removing the column filter), but
+# and finally, set the column list to ALL for one of the publications,
+# which means replicating all columns (removing the column list), but
 # first add the missing column to the table on subscriber
 $node_publisher->safe_psql('postgres', qq(
 	ALTER PUBLICATION pub3 ALTER TABLE tab5 SET COLUMNS ALL;
@@ -314,9 +314,9 @@ sub wait_for_subscription_sync
 3|33|3333|333),
    'overlapping publications with overlapping column lists');
 
-# TEST: create a table with a column filter, then change the replica
+# TEST: create a table with a column list, then change the replica
 # identity by replacing a primary key (but use a different column in
-# the column filter)
+# the column list)
 $node_publisher->safe_psql('postgres', qq(
 	CREATE TABLE tab6 (a int PRIMARY KEY, b int, c int, d int);
 	CREATE PUBLICATION pub4 FOR TABLE tab6 (a, b);
@@ -377,9 +377,9 @@ sub wait_for_subscription_sync
    'replication with the modified primary key');

-# TEST: create a table with a column filter, then change the replica
+# TEST: create a table with a column list, then change the replica
 # identity by replacing a primary key with a key on multiple columns
-# (all of them covered by the column filter)
+# (all of them covered by the column list)
 $node_publisher->safe_psql('postgres', qq(
 	CREATE TABLE tab7 (a int PRIMARY KEY, b int, c int, d int);
 	CREATE PUBLICATION pub5 FOR TABLE tab7 (a, b);
@@ -425,7 +425,7 @@ sub wait_for_subscription_sync
    'replication with the modified primary key');
 
 # now switch the primary key again to another columns not covered by the
-# column filter, but also generate writes between the drop and creation
+# column list, but also generate writes between the drop and creation
 # of the new constraint
 
 $node_publisher->safe_psql('postgres', qq(
@@ -450,11 +450,11 @@ sub wait_for_subscription_sync
 
 # TEST: partitioned tables (with publish_via_partition_root = false)
 # and replica identity. The (leaf) partitions may have different RI, so
-# we need to check the partition RI (with respect to the column filter)
+# we need to check the partition RI (with respect to the column list)
 # while attaching the partition.
 
 # First, let's create a partitioned table with two partitions, each with
-# a different RI, but a column filter not covering all those RI.
+# a different RI, but a column list not covering all those RI.
 
 $node_publisher->safe_psql('postgres', qq(
 	CREATE TABLE test_part_a (a int, b int, c int) PARTITION BY LIST (a);
@@ -508,8 +508,8 @@ sub wait_for_subscription_sync
 2|4),
    'partitions with different replica identities not replicated correctly');
 
-# This time start with a column filter covering RI for all partitions, but
-# then update the column filter to not cover column "b" (needed by the
+# This time start with a column list covering RI for all partitions, but
+# then update the column list to not cover column "b" (needed by the
 # second partition)
 
 $node_publisher->safe_psql('postgres', qq(
@@ -563,9 +563,9 @@ sub wait_for_subscription_sync
    'partitions with different replica identities not replicated correctly');

-# TEST: This time start with a column filter covering RI for all partitions,
+# TEST: This time start with a column list covering RI for all partitions,
 # but then update RI for one of the partitions to not be covered by the
-# column filter anymore.
+# column list anymore.
 
 $node_publisher->safe_psql('postgres', qq(
 	CREATE TABLE test_part_c (a int, b int, c int) PARTITION BY LIST (a);
@@ -594,7 +594,7 @@ sub wait_for_subscription_sync
 
 # create a publication replicating data through partition root, with a column
 # filter on the root, and then add the partitions one by one with separate
-# column filters (but those are not applied)
+# column lists (but those are not applied)
 $node_publisher->safe_psql('postgres', qq(
 	CREATE PUBLICATION pub8 FOR TABLE test_part_c WITH (publish_via_partition_root = false);
 	ALTER PUBLICATION pub8 ADD TABLE test_part_c_1 (a,c);
@@ -623,8 +623,8 @@ sub wait_for_subscription_sync

 # create a publication not replicating data through partition root, without
-# a column filter on the root, and then add the partitions one by one with
-# separate column filters
+# a column list on the root, and then add the partitions one by one with
+# separate column lists
 $node_publisher->safe_psql('postgres', qq(
 	DROP PUBLICATION pub8;
 	CREATE PUBLICATION pub8 FOR TABLE test_part_c WITH (publish_via_partition_root = false);
@@ -703,7 +703,7 @@ sub wait_for_subscription_sync
    'partitions with different replica identities not replicated correctly');
 
 # TEST: With a table included in multiple publications, we should use a
-# union of the column filters. So with column filters (a,b) and (a,c) we
+# union of the column lists. So with column lists (a,b) and (a,c) we
 # should replicate (a,b,c).
 
 $node_publisher->safe_psql('postgres', qq(
@@ -727,11 +727,11 @@ sub wait_for_subscription_sync
 
 is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_1"),
    qq(1|2|3),
-   'a mix of publications should use a union of column filter');
+   'a mix of publications should use a union of column list');

 # TEST: With a table included in multiple publications, we should use a
-# union of the column filters. If any of the publications is FOR ALL
+# union of the column lists. If any of the publications is FOR ALL
 # TABLES, we should replicate all columns.
 
 # drop unnecessary tables, so as not to interfere with the FOR ALL TABLES
@@ -762,11 +762,11 @@ sub wait_for_subscription_sync
 
 is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_2"),
    qq(1|2|3),
-   'a mix of publications should use a union of column filter');
+   'a mix of publications should use a union of column list');

 # TEST: With a table included in multiple publications, we should use a
-# union of the column filters. If any of the publications is FOR ALL
+# union of the column lists. If any of the publications is FOR ALL
 # TABLES IN SCHEMA, we should replicate all columns.
 
 $node_publisher->safe_psql('postgres', qq(
@@ -790,11 +790,11 @@ sub wait_for_subscription_sync
 
 is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_3"),
    qq(1|2|3),
-   'a mix of publications should use a union of column filter');
+   'a mix of publications should use a union of column list');

 # TEST: Check handling of publish_via_partition_root - if a partition is
-# published through partition root, we should only apply the column filter
+# published through partition root, we should only apply the column list
 # defined for the whole table (not the partitions) - both during the initial
 # sync and when replicating changes. This is what we do for row filters.
 
@@ -828,7 +828,7 @@ sub wait_for_subscription_sync
 is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_root ORDER BY a, b, c"),
    qq(1||
 10||),
-   'publication via partition root applies column filter');
+   'publication via partition root applies column list');
 
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
-- 
2.35.1

---

From: Amit Kapila <[email-masked]>
Date: Wed, 9 Mar 2022 15:33:40 +0530

On Mon, Mar 7, 2022 at 8:48 PM Tomas Vondra
<[email-masked]> wrote:
> data?

Yes.

> OK, I reworked this to do the same thing as the row filtering patch.
>

Thanks, I'll check this.

> the row-filtering patch, right?

Right.

> the same ambiguity issue.

I think we should fix this existing problem by always using the
top-most table as publish_as_relid. Basically, we can check, if the
existing publish_as_relid is an ancestor of a new rel that is going to
replace it then we shouldn't replace it. However, I think even if we
fix the existing problem, we will still have the order problem for the
column filter patch, and to avoid that instead of fetching column
filters in the publication loop, we should use the final
publish_as_relid. I think it will have another problem as well if we
don't use final publish_as_relid which is that sometimes when we
should not use any filter (say when pubviaroot is true and that
publication has root partitioned table which has no filter) as per our
rule of filters for a partitioned table, it can still use some filter
from the non-root table.

> Maybe, but I really don't think this is an issue.
>

I am not sure but it might matter for small tables. Leaving aside the
performance issue, I think the current way will get the wrong column
list in many cases: (a) The ALL TABLES IN SCHEMA case handling won't
work for partitioned tables when the partitioned table is part of one
schema and partition table is part of another schema. (b) The handling
of partition tables in other cases will fetch incorrect lists as it
tries to fetch the column list of all the partitions in the hierarchy.

One of my colleagues has even tested these cases both for column
filters and row filters and we find the behavior of row filter is okay
whereas for column filter it uses the wrong column list. We will share
the tests and results with you in a later email. We are trying to
unify the column filter queries with row filter to make their behavior
the same and will share the findings once it is done. I hope if we are
able to achieve this that we will reduce the chances of bugs in this
area.

Note: I think the first two patches for tests are not required after
commit ceb57afd3c.

-- 
With Regards,
Amit Kapila.

---

From: "[email-masked]" <[email-masked]>
Date: Wed, 9 Mar 2022 10:12:01 +0000

On Wednesday, March 9, 2022 6:04 PM Amit Kapila <[email-masked]>
> performance issue, I think the current way will get the wrong column list in
> many cases: (a) The ALL TABLES IN SCHEMA case handling won't work for
> partitioned tables when the partitioned table is part of one schema and
> partition table is part of another schema. (b) The handling of partition tables in
> other cases will fetch incorrect lists as it tries to fetch the column list of all the
> partitions in the hierarchy.
> 
> One of my colleagues has even tested these cases both for column filters and
> row filters and we find the behavior of row filter is okay whereas for column
> filter it uses the wrong column list. We will share the tests and results with you
> in a later email. We are trying to unify the column filter queries with row filter to
> make their behavior the same and will share the findings once it is done. I hope
> if we are able to achieve this that we will reduce the chances of bugs in this area.
> 
> Note: I think the first two patches for tests are not required after commit
> ceb57afd3c.

Hi,

Here are some tests and results about the table sync query of
column filter patch and row filter.

1) multiple publications which publish schema of parent table and partition.
----pub
create schema s1;
create table s1.t (a int, b int, c int) partition by range (a);
create table t_1 partition of s1.t for values from (1) to (10);
create publication pub1 for all tables in schema s1;
create publication pub2 for table t_1(b);

----sub
- prepare tables
CREATE SUBSCRIPTION sub CONNECTION 'port=10000 dbname=postgres' PUBLICATION pub1, pub2;

When doing table sync for 't_1', the column list will be (b). I think it should
be no filter because table t_1 is also published via ALL TABLES IN SCHMEA
publication.

For Row Filter, it will use no filter for this case.

2) one publication publishes both parent and child
----pub
create table t (a int, b int, c int) partition by range (a);
create table t_1 partition of t for values from (1) to (10)
       partition by range (a);
create table t_2 partition of t_1 for values from (1) to (10);

create publication pub2 for table t_1(a), t_2
  with (PUBLISH_VIA_PARTITION_ROOT);

----sub
- prepare tables
CREATE SUBSCRIPTION sub CONNECTION 'port=10000 dbname=postgres' PUBLICATION pub2;

When doing table sync for table 't_1', it has no column list. I think the
expected column list is (a).

For Row Filter, it will use the row filter of the top most parent table(t_1) in
this case.

3) one publication publishes both parent and child
----pub
create table t (a int, b int, c int) partition by range (a);
create table t_1 partition of t for values from (1) to (10)
       partition by range (a);
create table t_2 partition of t_1 for values from (1) to (10);

create publication pub2 for table t_1(a), t_2(b)
  with (PUBLISH_VIA_PARTITION_ROOT);

----sub
- prepare tables
CREATE SUBSCRIPTION sub CONNECTION 'port=10000 dbname=postgres' PUBLICATION pub2;

When doing table sync for table 't_1', the column list would be (a, b). I think
the expected column list is (a).

For Row Filter, it will use the row filter of the top most parent table(t_1) in
this case.

Best regards,
Hou zj

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 9 Mar 2022 14:53:12 +0100

On 3/9/22 11:03, Amit Kapila wrote:
> replace it then we shouldn't replace it.

Right, using the topmost relid from all publications seems like the
correct solution.

> However, I think even if we

Yeah, the current behavior is just a consequence of how we determine
publish_as_relid now. If we rework that, we should first determine the
relid and then fetch the filter only for that single rel.

> able to achieve this that we will reduce the chances of bugs in this

OK, I'll take a look at that email.

> Note: I think the first two patches for tests are not required after

Right. Will remove.

regards

---

From: Amit Kapila <[email-masked]>
Date: Thu, 10 Mar 2022 08:39:46 +0530

> > OK, I reworked this to do the same thing as the row filtering patch.

Some assorted comments:
=====================
1. We don't need to send a column list for the old tuple in case of an
update (similar to delete). It is not required to apply a column
filter for those cases because we ensure that RI must be part of the
column list for updates and deletes.
2.
+ /*
+ * Check if all columns referenced in the column filter are part of
+ * the REPLICA IDENTITY index or not.

I think this comment is reverse. The rule we follow here is that
attributes that are part of RI must be there in a specified column
list. This is used at two places in the patch.
3. get_rel_sync_entry()
{
/* XXX is there a danger of memory leak here? beware */
+ oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+ for (int i = 0; i < nelems; i++)
...
}

Similar to the row filter, I think we need to use
entry->cache_expr_cxt to allocate this. There are other usages of
CacheMemoryContext in this part of the code but I think those need to
be also changed and we can do that as a separate patch. If we do the
suggested change then we don't need to separately free columns.
4. I think we don't need the DDL changes in AtExecDropColumn. Instead,
we can change the dependency of columns to NORMAL during publication
commands.
5. There is a reference to check_publication_columns but that function
is removed from the patch.
6.
/*
* If we know everything is replicated and the row filter is invalid
* for update and delete, there is no point to check for other
* publications.
*/
if (pubdesc->pubactions.pubinsert && pubdesc->pubactions.pubupdate &&
pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
!pubdesc->rf_valid_for_update && !pubdesc->rf_valid_for_delete)
break;

/*
* If we know everything is replicated and the column filter is invalid
* for update and delete, there is no point to check for other
* publications.
*/
if (pubdesc->pubactions.pubinsert && pubdesc->pubactions.pubupdate &&
pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
!pubdesc->cf_valid_for_update && !pubdesc->cf_valid_for_delete)
break;

Can we combine these two checks?

I feel this patch needs a more thorough review.

-- 
With Regards,
Amit Kapila.

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 10 Mar 2022 19:17:15 +0100

> For Row Filter, it will use the row filter of the top most parent table(t_1) in

Attached is an updated patch version, addressing all of those issues.

0001 is a bugfix, reworking how we calculate publish_as_relid. The old
approach was unstable with multiple publications, giving different
results depending on order of the publications. This should be
backpatched into PG13 where publish_via_partition_root was introduced, I
think.

0002 is the main patch, merging the changes proposed by Peter and fixing
the issues reported here. In most cases this means adopting the code
used for row filters, and perhaps simplifying it a bit.

But I also tried to implement a row-filter test for 0001, and I'm not
sure I understand the behavior I observe. Consider this:

-- a chain of 3 partitions (on both publisher and subscriber)
CREATE TABLE test_part_rf (a int primary key, b int, c int)
       PARTITION BY LIST (a);

CREATE TABLE test_part_rf_1
       PARTITION OF test_part_rf FOR VALUES IN (1,2,3,4,5)
       PARTITION BY LIST (a);

CREATE TABLE test_part_rf_2
       PARTITION OF test_part_rf_1 FOR VALUES IN (1,2,3,4,5);

-- initial data
INSERT INTO test_part_rf VALUES (1, 5, 100);
INSERT INTO test_part_rf VALUES (2, 15, 200);

-- two publications, each adding a different partition
CREATE PUBLICATION test_pub_part_1 FOR TABLE test_part_rf_1
 WHERE (b < 10) WITH (publish_via_partition_root);

CREATE PUBLICATION test_pub_part_2 FOR TABLE test_part_rf_2
 WHERE (b > 10) WITH (publish_via_partition_root);

-- now create the subscription (also try opposite ordering)
CREATE SUBSCRIPTION test_part_sub CONNECTION '...'
       PUBLICATION test_pub_part_1, test_pub_part_2;

-- wait for sync

-- inert some more data
INSERT INTO test_part_rf VALUES (3, 6, 300);
INSERT INTO test_part_rf VALUES (4, 16, 400);

-- wait for catchup

Now, based on the discussion here, my expectation is that we'll use the
row filter from the top-most ancestor in any publication, which in this
case is test_part_rf_1. Hence the filter should be (b < 10).

So I'd expect these rows to be replicated:

1,5,100
3,6,300

But that's not what I get, unfortunately. I get different results,
depending on the order of publications:

1) test_pub_part_1, test_pub_part_2

1|5|100
2|15|200
3|6|300
4|16|400

2) test_pub_part_2, test_pub_part_1

3|6|300
4|16|400

That seems pretty bizarre, because it either means we're not enforcing
any filter or some strange combination of filters (notice that for (2)
we skip/replicate rows matching either filter).

I have to be missing something important, but this seems confusing.
There's a patch adding a simple test case to 028_row_filter.sql (named
.txt, so as not to confuse cfbot).

FWIW I'm not convinced applying just the filters (both row and column)
is the right approach. It might be OK for a single publication, but with
multiple publications not so much. If you list multiple publications for
a subscription, it seems natural to expect a union of all the data, a
bit as if there were multiple subscriptions. But what you actually get
is some subset, depending on what other relations the other publications
include.

Of course, this only happens if the publications include different
ancestors. If all include the same ancestor, everything works fine and
you get the "union" of data.

regards

diff --git a/src/test/subscription/t/028_row_filter.pl b/src/test/subscription/t/028_row_filter.pl
index 89bb364e9da..333895e081b 100644
--- a/src/test/subscription/t/028_row_filter.pl
+++ b/src/test/subscription/t/028_row_filter.pl
@@ -691,6 +691,75 @@ is($result, qq(t|1), 'check replicated rows to tab_rowfilter_toast');
 # Testcase end: FOR TABLE with row filter publications
 # ======================================================
 
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_rf (a int primary key, b int, c int) PARTITION BY LIST (a);
+	CREATE TABLE test_part_rf_1 PARTITION OF test_part_rf FOR VALUES IN (1,2,3,4,5) PARTITION BY LIST (a);
+	CREATE TABLE test_part_rf_2 PARTITION OF test_part_rf_1 FOR VALUES IN (1,2,3,4,5);
+
+	CREATE PUBLICATION test_pub_part_1 FOR TABLE test_part_rf_1 WHERE (b < 10) WITH (publish_via_partition_root);
+	CREATE PUBLICATION test_pub_part_2 FOR TABLE test_part_rf_2 WHERE (b > 10) WITH (publish_via_partition_root);
+
+	INSERT INTO test_part_rf VALUES (1, 5, 100);
+	INSERT INTO test_part_rf VALUES (2, 15, 200);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_rf (a int primary key, b int, c int) PARTITION BY LIST (a);
+	CREATE TABLE test_part_rf_1 PARTITION OF test_part_rf FOR VALUES IN (1,2,3,4,5) PARTITION BY LIST (a);
+	CREATE TABLE test_part_rf_2 PARTITION OF test_part_rf_1 FOR VALUES IN (1,2,3,4,5);
+
+	CREATE SUBSCRIPTION test_part_sub CONNECTION '$publisher_connstr' PUBLICATION test_pub_part_1, test_pub_part_2;
+));
+
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_rf VALUES (3, 6, 300);
+	INSERT INTO test_part_rf VALUES (4, 16, 400);
+));
+
+$node_publisher->wait_for_catchup('test_part_sub');
+
+$result = $node_subscriber->safe_psql('postgres', qq(SELECT * FROM test_part_rf ORDER BY a));
+is($result,
+  qq(1|5|100
+3|6|300), 'check replicated rows with multiple row filters');
+
+
+$node_publisher->safe_psql('postgres',
+	qq(TRUNCATE test_part_rf));
+
+$node_publisher->wait_for_catchup('test_part_sub');
+
+$node_subscriber->safe_psql('postgres', qq(
+	DROP SUBSCRIPTION test_part_sub));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_rf VALUES (1, 5, 100);
+	INSERT INTO test_part_rf VALUES (2, 15, 200);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE SUBSCRIPTION test_part_sub CONNECTION '$publisher_connstr' PUBLICATION test_pub_part_2, test_pub_part_1;
+));
+
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_publisher->safe_psql('postgres', qq(
+	TRUNCATE test_part_rf;
+	INSERT INTO test_part_rf VALUES (3, 6, 300);
+	INSERT INTO test_part_rf VALUES (4, 16, 400);
+));
+
+$node_publisher->wait_for_catchup('test_part_sub');
+
+$result = $node_subscriber->safe_psql('postgres', qq(SELECT * FROM test_part_rf ORDER BY a));
+is($result,
+  qq(1|5|100
+3|6|300), 'check replicated rows with multiple row filters');
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
From 5070a26fb54b956557f4ba3a44b796ab4c232c93 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 9 Mar 2022 18:10:56 +0100
Subject: [PATCH 1/2] fixup: publish_as_relid

---
 src/backend/replication/pgoutput/pgoutput.c | 32 +++++++++++++++++++--
 1 file changed, 30 insertions(+), 2 deletions(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index ea57a0477f0..dbac2690b7f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1815,11 +1815,17 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Publication *pub = lfirst(lc);
 			bool		publish = false;
 
+			/*
+			 * Under what relid should we publish changes in this publication?
+			 * We'll use the top-most relid across all publications.
+			 */
+			Oid			pub_relid = relid;
+
 			if (pub->alltables)
 			{
 				publish = true;
 				if (pub->pubviaroot && am_partition)
-					publish_as_relid = llast_oid(get_partition_ancestors(relid));
+					pub_relid = llast_oid(get_partition_ancestors(relid));
 			}
 
 			if (!publish)
@@ -1844,7 +1850,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 					{
 						ancestor_published = true;
 						if (pub->pubviaroot)
-							publish_as_relid = ancestor;
+							pub_relid = ancestor;
 					}
 				}
 
@@ -1862,12 +1868,34 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			if (publish &&
 				(relkind != RELKIND_PARTITIONED_TABLE || pub->pubviaroot))
 			{
+				List *ancestors;
+
 				entry->pubactions.pubinsert |= pub->pubactions.pubinsert;
 				entry->pubactions.pubupdate |= pub->pubactions.pubupdate;
 				entry->pubactions.pubdelete |= pub->pubactions.pubdelete;
 				entry->pubactions.pubtruncate |= pub->pubactions.pubtruncate;
 
 				rel_publications = lappend(rel_publications, pub);
+
+				/*
+				 * We want to publish the changes as the top-most ancestor
+				 * across all publications. So we fetch all ancestors of the
+				 * relid calculated for this publication, and check if the
+				 * already calculated value is in the list. If yes, we can
+				 * ignore the new value (as it's a child). Otherwise the new
+				 * value is an ancestor, so we keep it.
+				 */
+				ancestors = get_partition_ancestors(pub_relid);
+
+				/*
+				 * The new pub_relid is a child of the current publish_as_relid
+				 * value, so we can ignore it.
+				 */
+				if (list_member_oid(ancestors, publish_as_relid))
+					continue;
+
+				/* The new value is an ancestor, so let's keep it. */
+				publish_as_relid = pub_relid;
 			}
 		}
 
-- 
2.34.1

From 19a580e1d8e5772cd18b933cc500bba36d54fffc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Thu, 10 Mar 2022 17:31:57 +0100
Subject: [PATCH 2/2] Allow specifying column filters for logical replication

Author: Rahila Syed <[email-masked]>
Discussion: https://postgr.es/m/CAH2L28vddB_NFdRVpuyRBJEBWjz4BSyTB=[email-masked]
---
 doc/src/sgml/catalogs.sgml                  |   15 +-
 doc/src/sgml/protocol.sgml                  |    3 +-
 doc/src/sgml/ref/alter_publication.sgml     |   27 +-
 doc/src/sgml/ref/create_publication.sgml    |   17 +-
 src/backend/catalog/pg_publication.c        |  256 +++++
 src/backend/commands/publicationcmds.c      |  364 +++++-
 src/backend/commands/tablecmds.c            |   36 +-
 src/backend/executor/execReplication.c      |   19 +-
 src/backend/nodes/copyfuncs.c               |    1 +
 src/backend/nodes/equalfuncs.c              |    1 +
 src/backend/parser/gram.y                   |   60 +-
 src/backend/replication/logical/proto.c     |   56 +-
 src/backend/replication/logical/tablesync.c |  184 ++-
 src/backend/replication/pgoutput/pgoutput.c |  156 ++-
 src/backend/utils/cache/relcache.c          |   33 +-
 src/bin/pg_dump/pg_dump.c                   |   47 +-
 src/bin/pg_dump/pg_dump.h                   |    1 +
 src/bin/pg_dump/t/002_pg_dump.pl            |   60 +
 src/bin/psql/describe.c                     |   40 +-
 src/include/catalog/pg_publication.h        |   13 +
 src/include/catalog/pg_publication_rel.h    |    1 +
 src/include/commands/publicationcmds.h      |    4 +-
 src/include/nodes/parsenodes.h              |    4 +-
 src/include/replication/logicalproto.h      |    6 +-
 src/test/regress/expected/publication.out   |  375 +++++++
 src/test/regress/sql/publication.sql        |  290 +++++
 src/test/subscription/t/029_column_list.pl  | 1124 +++++++++++++++++++
 27 files changed, 3112 insertions(+), 81 deletions(-)
 create mode 100644 src/test/subscription/t/029_column_list.pl

index 83987a99045..c043da37aee 100644
index 0695bcd423e..92cd0f9c9f7 100644
@@ -7006,7 +7006,8 @@ Relation
index 32b75f6c78e..470d50a2447 100644

 
+  <para>
@@ -112,6 +119,14 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
@@ -172,9 +187,15 @@ ALTER PUBLICATION noinsert SET (publish = 'update, delete');
   </para>
 
   <para>
 
   <para>

 
+     <para>
 
index 25998fbb39b..3275a7c8b9c 100644
 
+static void publication_translate_columns(Relation targetrel, List *columns,
+										  int *natts, AttrNumber **attrs);
+
 /*
  * Check if relation can be in given publication and throws appropriate
  * error if not.
@@ -328,6 +331,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	Oid			relid = RelationGetRelid(targetrel);
 	Oid			pubreloid;
 	Publication *pub = GetPublication(pubid);
+	AttrNumber *attarray;
+	int			natts = 0;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
@@ -355,6 +360,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,

+	/*
@@ -367,6 +380,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 
@@ -382,6 +406,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 
@@ -415,6 +447,154 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	return myself;
 }
 
+/*
@@ -522,6 +702,82 @@ GetRelationPublications(Oid relid)
 	return result;
 }
 
+/*
index 16b8661a1b7..b32ec275557 100644
 
+/*
 
+
+/*

+/*
 
 	/*

+			has_row_filter

 

+	/*

 

 
+			/*

 
 			foreach(newlc, rels)
 			{

 				/*
 
 /*
 
 	/*

 		if (t->whereClause)
 
+		if (t->columns)
 
+					/*

 				if (t->whereClause)
 
+		if (pubrel->columns)
+			ereport(ERROR,
 
 	ReleaseSysCache(tuple);
 
+	/*
 
 	if (children)
 	{

index 09f78f22441..3e282ed99ab 100644

 
 	 * This is UPDATE/DELETE and there is no replica identity.

 	return newnode;
 }
 
 	return true;
 }
 
index c9b0eeefd7e..816d461acd3 100644
@@ -29,16 +29,25 @@

 
+
+static bool
@@ -398,7 +407,7 @@ logicalrep_read_origin(StringInfo in, XLogRecPtr *origin_lsn)
 
@@ -410,7 +419,7 @@ logicalrep_write_insert(StringInfo out, TransactionId xid, Relation rel,
 	pq_sendint32(out, RelationGetRelid(rel));

 /*
@@ -443,7 +452,7 @@ logicalrep_read_insert(StringInfo in, LogicalRepTupleData *newtup)
 void
 
@@ -464,11 +473,11 @@ logicalrep_write_update(StringInfo out, TransactionId xid, Relation rel,

 /*
@@ -537,7 +546,7 @@ logicalrep_write_delete(StringInfo out, TransactionId xid, Relation rel,
 	else
 		pq_sendbyte(out, 'K');	/* old key follows */

 /*
@@ -652,7 +661,8 @@ logicalrep_write_message(StringInfo out, TransactionId xid, XLogRecPtr lsn,
  * Write relation description to the output stream.
  */
 void
-logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
+logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel,
+					 Bitmapset *columns)
 {
 	char	   *relname;
 
@@ -674,7 +684,7 @@ logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel)
 	pq_sendbyte(out, rel->rd_rel->relreplident);

 /*
@@ -751,7 +761,7 @@ logicalrep_read_typ(StringInfo in, LogicalRepTyp *ltyp)
  */
 static void
 logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
-					   bool binary)
+					   bool binary, Bitmapset *columns)
 {
 	TupleDesc	desc;
 	Datum	   *values;
@@ -763,8 +773,14 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 
@@ -783,6 +799,9 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 
@@ -904,7 +923,7 @@ logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple)
@@ -917,8 +936,14 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
@@ -937,6 +962,9 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 
index 1659964571c..6eb9fc902b7 100644
@@ -701,12 +702,13 @@ fetch_remote_table_info(char *nspname, char *relname,
 	Oid			tableRow[] = {OIDOID, CHAROID, CHAROID};
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;
@@ -747,10 +749,139 @@ fetch_remote_table_info(char *nspname, char *relname,
 	ExecDropSingleTupleTableSlot(slot);
 	walrcv_clear_result(res);
 
-	/* Now fetch columns. */
+
+	/*
+	 * Get column lists for each relation.
+	 *
+	 * For initial synchronization, column lists can be ignored in following
+	 * cases:
+	 *
+	 * 1) one of the subscribed publications for the table hasn't specified
+	 * any column list
+	 *
+	 * 2) one of the subscribed publications has puballtables set to true
+	 *
+	 * 3) one of the subscribed publications is declared as ALL TABLES IN
+	 * SCHEMA that includes this relation
+	 */
+	if (walrcv_server_version(LogRepWorkerWalRcvConn) >= 150000)
+	{
+		WalRcvExecResult *pubres;
+		TupleTableSlot *slot;
+		Oid			attrsRow[] = {INT2OID};
+		StringInfoData pub_names;
+		bool		first = true;
+		bool		all_columns = false;
+
+		initStringInfo(&pub_names);
+		foreach(lc, MySubscription->publications)
+		{
+			if (!first)
+				appendStringInfo(&pub_names, ", ");
+			appendStringInfoString(&pub_names, quote_literal_cstr(strVal(lfirst(lc))));
+			first = false;
+		}
+
+		/*
+		 * Check for column filters - we first check if there's any publication
+		 * that has no column list for the given relation, which means we shall
+		 * replicate all columns.
+		 *
+		 * It's easier than having to do this separately, and only then do the
+		 * second query 
+		 */
+		resetStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT 1"
+						 "  FROM pg_publication p"
+						 "  LEFT OUTER JOIN pg_publication_rel pr"
+						 "       ON (p.oid = pr.prpubid AND pr.prrelid = %u),"
+						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+						 " WHERE gpt.relid = %u"
+						 "   AND p.pubname IN ( %s ) AND pr.prattrs IS NULL LIMIT 1",
+						 lrel->remoteid,
+						 lrel->remoteid,
+						 pub_names.data);
+
+		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+							 lengthof(attrsRow), attrsRow);
+
+		if (pubres->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not fetch column list info for table \"%s.%s\" from publisher: %s",
+							nspname, relname, pubres->err)));
+
+		slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+		if (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+			all_columns = true;
+
+		ExecDropSingleTupleTableSlot(slot);
+		walrcv_clear_result(pubres);
+
+		if (!all_columns)
+		{
+			resetStringInfo(&cmd);
+			appendStringInfo(&cmd,
+							 "SELECT unnest(pr.prattrs)"
+							 "  FROM pg_publication p"
+							 "  LEFT OUTER JOIN pg_publication_rel pr"
+							 "       ON (p.oid = pr.prpubid AND pr.prrelid = %u),"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s ) AND pr.prattrs IS NOT NULL",
+							 lrel->remoteid,
+							 lrel->remoteid,
+							 pub_names.data);
+
+			pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+								 lengthof(attrsRow), attrsRow);
+
+			if (pubres->status != WALRCV_OK_TUPLES)
+				ereport(ERROR,
+						(errcode(ERRCODE_CONNECTION_FAILURE),
+						 errmsg("could not fetch column list info for table \"%s.%s\" from publisher: %s",
+								nspname, relname, pubres->err)));
+
+			/*
+			 * Multiple column list expressions for the same table will be combined
+			 * by merging them. If any of the lists for this table are null, it
+			 * means the whole table will be copied. In this case it is not necessary
+			 * to construct a unified column list expression at all.
+			 */
+			slot = MakeSingleTupleTableSlot(pubres->tupledesc, &TTSOpsMinimalTuple);
+			while (tuplestore_gettupleslot(pubres->tuplestore, true, false, slot))
+			{
+				Datum	cfval = slot_getattr(slot, 1, &isnull);
+
+				/* if there's no column list, we need to replicate all columns */
+				if (isnull)
+				{
+					bms_free(included_cols);
+					included_cols = NULL;
+					break;
+				}
+
+				included_cols = bms_add_member(included_cols,
+											   DatumGetInt16(cfval));
+
+				ExecClearTuple(slot);
+			}
+			ExecDropSingleTupleTableSlot(slot);
+
+			walrcv_clear_result(pubres);
+		}
+
+		pfree(pub_names.data);
+	}
+
+	/*
+	 * Now fetch column names and types.
+	 */
 	resetStringInfo(&cmd);
 	appendStringInfo(&cmd,
-					 "SELECT a.attname,"
+					 "SELECT a.attnum,"
+					 "       a.attname,"
 					 "       a.atttypid,"
 					 "       a.attnum = ANY(i.indkey)"
 					 "  FROM pg_catalog.pg_attribute a"
@@ -778,16 +909,34 @@ fetch_remote_table_info(char *nspname, char *relname,
 	lrel->atttyps = palloc0(MaxTupleAttributeNumber * sizeof(Oid));
 	lrel->attkeys = NULL;
 
+	/*
 
 		/* Should never happen. */
@@ -821,6 +970,9 @@ fetch_remote_table_info(char *nspname, char *relname,
 	 *
 	 * 3) one of the subscribed publications is declared as ALL TABLES IN
 	 * SCHEMA that includes this relation
+	 *
+	 * XXX Does this actually handle puballtables and schema publications
+	 * correctly?
 	 */
 	if (walrcv_server_version(LogRepWorkerWalRcvConn) >= 150000)
 	{
@@ -930,8 +1082,24 @@ copy_table(Relation rel)
 
index dbac2690b7f..fcdd7785a79 100644

 /* Map used to remember which relation schemas we sent. */
@@ -188,6 +199,7 @@ static EState *create_estate_for_relation(Relation rel);
 static void pgoutput_row_filter_init(PGOutputData *data,
 									 List *publications,
 									 RelationSyncEntry *entry);
+
 static bool pgoutput_row_filter_exec_expr(ExprState *state,
 										  ExprContext *econtext);
 static bool pgoutput_row_filter(Relation relation, TupleTableSlot *old_slot,
@@ -195,6 +207,11 @@ static bool pgoutput_row_filter(Relation relation, TupleTableSlot *old_slot,
 								RelationSyncEntry *entry,
 								ReorderBufferChangeType *action);
 
+/* column filter routines */
+static void pgoutput_column_filter_init(PGOutputData *data,
+										List *publications,
+										RelationSyncEntry *entry);
+
 /*
  * Specify output plugin callbacks
  */
@@ -603,11 +620,11 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 	{
 		Relation	ancestor = RelationIdGetRelation(relentry->publish_as_relid);

 
 	if (in_streaming)
 		set_schema_sent_in_streamed_txn(relentry, topxid);
@@ -620,7 +637,8 @@ maybe_send_schema(LogicalDecodingContext *ctx,
  */
 static void
 send_relation_and_attrs(Relation relation, TransactionId xid,
-						LogicalDecodingContext *ctx)
+						LogicalDecodingContext *ctx,
+						Bitmapset *columns)
 {
 	TupleDesc	desc = RelationGetDescr(relation);
 	int			i;
@@ -643,13 +661,17 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		if (att->atttypid < FirstGenbkiObjectId)
 			continue;

 
@@ -860,6 +882,108 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
 	}
 }
 
+/*
+ * Initialize the column filter.
+ */
+static void
+pgoutput_column_filter_init(PGOutputData *data, List *publications,
+							RelationSyncEntry *entry)
+{
+	ListCell   *lc;
+	MemoryContext oldctx;
+
+	/*
+	 * Find if there are any row filters for this relation. If there are, then
+	 * prepare the necessary ExprState and cache it in entry->exprstate. To
+	 * build an expression state, we need to ensure the following:
+	 *
+	 * All the given publication-table mappings must be checked.
+	 *
+	 * Multiple publications might have multiple row filters for this
+	 * relation. Since row filter usage depends on the DML operation, there
+	 * are multiple lists (one for each operation) to which row filters will
+	 * be appended.
+	 *
+	 * FOR ALL TABLES implies "don't use row filter expression" so it takes
+	 * precedence.
+	 */
+	foreach(lc, publications)
+	{
+		Publication *pub = lfirst(lc);
+		HeapTuple	cftuple = NULL;
+		Datum		cfdatum = 0;
+		bool		pub_no_filter = false;
+
+		if (pub->alltables)
+		{
+			/*
+			 * If the publication is FOR ALL TABLES then it is treated the
+			 * same as if this table has no row filters (even if for other
+			 * publications it does).
+			 */
+			pub_no_filter = true;
+		}
+		else
+		{
+			/*
+			 * Check for the presence of a row filter in this publication.
+			 */
+			cftuple = SearchSysCache2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(entry->publish_as_relid),
+									  ObjectIdGetDatum(pub->oid));
+
+			if (HeapTupleIsValid(cftuple))
+			{
+				/* Null indicates no filter. */
+				cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+										  Anum_pg_publication_rel_prattrs,
+										  &pub_no_filter);
+
+				/*
+				 * When no column list is defined, so publish all columns.
+				 * Otherwise merge the columns to the column list.
+				 */
+				if (!pub_no_filter)	/* when not null */
+				{
+					ArrayType  *arr;
+					int			nelems;
+					int16	   *elems;
+
+					arr = DatumGetArrayTypeP(cfdatum);
+					nelems = ARR_DIMS(arr)[0];
+					elems = (int16 *) ARR_DATA_PTR(arr);
+
+					/* XXX is there a danger of memory leak here? beware */
+					oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+					for (int i = 0; i < nelems; i++)
+						entry->columns = bms_add_member(entry->columns,
+														elems[i]);
+					MemoryContextSwitchTo(oldctx);
+				}
+			}
+			else
+			{
+				pub_no_filter = true;
+			}
+		}
+
+		/* found publication with no filter, so we're done */
+		if (pub_no_filter)
+		{
+			if (cftuple)
+				ReleaseSysCache(cftuple);
+
+			bms_free(entry->columns);
+			entry->columns = NULL;
+
+			break;
+		}
+
+		ReleaseSysCache(cftuple);
+	}							/* loop all subscribed publications */
+
+}
+
 /*
  * Initialize the slot for storing new and old tuples, and build the map that
  * will be used to convert the relation's tuples into the ancestor's format.
@@ -1224,7 +1348,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
@@ -1278,11 +1402,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
@@ -1731,6 +1857,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
@@ -1775,6 +1902,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->schema_sent = false;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
+		bms_free(entry->columns);
+		entry->columns = NULL;
 		entry->pubactions.pubinsert = false;
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
@@ -1807,8 +1936,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		/*
@@ -1861,6 +1991,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			}
 
 			/*
@@ -1913,6 +2046,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 			/* Initialize the row filter */
 			pgoutput_row_filter_init(data, rel_publications, entry);
+
+			/* Initialize the column filter */
+			pgoutput_column_filter_init(data, rel_publications, entry);
 		}
 
 		list_free(pubids);
index fccffce5729..a2da72f0d48 100644

 	/* Fetch the publication membership info. */
 	puboids = GetRelationPublications(relid);
@@ -5616,7 +5620,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		 */
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
-			contain_invalid_rfcolumn(pubid, relation, ancestors,
+			pub_rf_contains_invalid_column(pubid, relation, ancestors,
 									 pubform->pubviaroot))
 		{
 			if (pubform->pubupdate)
@@ -5625,6 +5629,23 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 				pubdesc->rf_valid_for_delete = false;
 		}
 
+		/*
 
 		/*
@@ -5636,6 +5657,16 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)

 

 

 /*
index 3e55ff26f82..ed57c53bcb5 100644

index e3382933d98..e462ccfb748 100644

 
index ba72e62e614..a06742a6200 100644
@@ -85,6 +85,13 @@ typedef struct PublicationDesc
 
 typedef struct Publication
@@ -100,6 +107,7 @@ typedef struct PublicationRelInfo
 {
 	Relation	relation;
 	Node	   *whereClause;
+	List	   *columns;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
@@ -123,8 +131,11 @@ typedef enum PublicationPartOpt
 } PublicationPartOpt;
 
@@ -142,6 +153,8 @@ extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri

 
index 7813cbcb6bb..ae87caf089d 100644
@@ -31,7 +31,9 @@ extern void RemovePublicationSchemaById(Oid psoid);

 /*
 
index 4e191c120ac..79ced2921b6 100644
@@ -613,6 +613,372 @@ DROP TABLE rf_tbl_abcd_pk;
@@ -1058,6 +1424,15 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_tes
 Tables from schemas:
     "pub_test1"
 
index 5457c56b33f..be05ac9f763 100644
@@ -373,6 +373,292 @@ DROP TABLE rf_tbl_abcd_nopk;
 
@@ -614,6 +900,10 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA non_existent_schem
 
+-- Verify that it fails to add a schema with a column specification
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo (a, b);
+ALTER PUBLICATION testpub1_forschema ADD ALL TABLES IN SCHEMA foo, bar (a, b);
+
 -- cleanup pub_test1 schema for invalidation tests
 ALTER PUBLICATION testpub2_forschema DROP ALL TABLES IN SCHEMA pub_test1;
 DROP PUBLICATION testpub3_forschema, testpub4_forschema, testpub5_forschema, testpub6_forschema, testpub_fortable;
diff --git a/src/test/subscription/t/029_column_list.pl b/src/test/subscription/t/029_column_list.pl
new file mode 100644
index 00000000000..5266967b3f4
--- /dev/null
+++ b/src/test/subscription/t/029_column_list.pl
@@ -0,0 +1,1124 @@
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+# Test partial-column publication of tables
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# create publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->append_conf('postgresql.conf',
+	qq(max_logical_replication_workers = 6));
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+sub wait_for_subscription_sync
+{
+	my ($node) = @_;
+
+	# Also wait for initial table sync to finish
+	my $synced_query = "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+
+	$node->poll_query_until('postgres', $synced_query)
+		or die "Timed out while waiting for subscriber to synchronize data";
+}
+
+# setup tables on both nodes
+
+# tab1: simple 1:1 replication
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab1 (a int PRIMARY KEY, "B" int, c int)
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab1 (a int PRIMARY KEY, "B" int, c int)
+));
+
+# tab2: replication from regular to table with fewer columns
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab2 (a int PRIMARY KEY, b varchar, c int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab2 (a int PRIMARY KEY, b varchar)
+));
+
+# tab3: simple 1:1 replication with weird column names
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab3 ("a'" int PRIMARY KEY, "B" varchar, "c'" int)
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab3 ("a'" int PRIMARY KEY, "c'" int)
+));
+
+# test_part: partitioned tables, with partitioning (including multi-level
+# partitioning, and fewer columns on the subscriber)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part (a int PRIMARY KEY, b text, c timestamptz) PARTITION BY LIST (a);
+	CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3,4,5,6);
+	CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (7,8,9,10,11,12) PARTITION BY LIST (a);
+	CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (7,8,9,10);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part (a int PRIMARY KEY, b text) PARTITION BY LIST (a);
+	CREATE TABLE test_part_1_1 PARTITION OF test_part FOR VALUES IN (1,2,3,4,5,6);
+	CREATE TABLE test_part_2_1 PARTITION OF test_part FOR VALUES IN (7,8,9,10,11,12) PARTITION BY LIST (a);
+	CREATE TABLE test_part_2_2 PARTITION OF test_part_2_1 FOR VALUES IN (7,8,9,10);
+));
+
+# tab4: table with user-defined enum types
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TYPE test_typ AS ENUM ('blue', 'red');
+	CREATE TABLE tab4 (a INT PRIMARY KEY, b test_typ, c int, d text);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TYPE test_typ AS ENUM ('blue', 'red');
+	CREATE TABLE tab4 (a INT PRIMARY KEY, b test_typ, d text);
+));
+
+
+# TEST: create publication and subscription for some of the tables with
+# column lists
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub1
+	   FOR TABLE tab1 (a, "B"), tab3 ("a'", "c'"), test_part (a, b), tab4 (a, b, d)
+	  WITH (publish_via_partition_root = 'true');
+));
+
+# check that we got the right prattrs values for the publication in the
+# pg_publication_rel catalog (order by relname, to get stable ordering)
+my $result = $node_publisher->safe_psql('postgres', qq(
+	SELECT relname, prattrs
+	FROM pg_publication_rel pb JOIN pg_class pc ON(pb.prrelid = pc.oid)
+	ORDER BY relname
+));
+
+is($result, qq(tab1|1 2
+tab3|1 3
+tab4|1 2 4
+test_part|1 2), 'publication relation updated');
+
+# TEST: insert data into the tables, create subscription and see if sync
+# replicates the right columns
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO tab1 VALUES (4, 5, 6);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab3 VALUES (1, 2, 3);
+	INSERT INTO tab3 VALUES (4, 5, 6);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab4 VALUES (1, 'red', 3, 'oh my');
+	INSERT INTO tab4 VALUES (2, 'blue', 4, 'hello');
+));
+
+# replication of partitioned table
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part VALUES (1, 'abc', '2021-07-04 12:00:00');
+	INSERT INTO test_part VALUES (2, 'bcd', '2021-07-03 11:12:13');
+	INSERT INTO test_part VALUES (7, 'abc', '2021-07-04 12:00:00');
+	INSERT INTO test_part VALUES (8, 'bcd', '2021-07-03 11:12:13');
+));
+
+# create subscription for the publication, wait for sync to complete,
+# then check the sync results
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+# tab1: only (a,b) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab1 ORDER BY a");
+is($result, qq(1|2|
+4|5|), 'insert on column tab1.c is not replicated');
+
+# tab3: only (a,c) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab3 ORDER BY "a'"));
+is($result, qq(1|3
+4|6), 'insert on column tab3.b is not replicated');
+
+# tab4: only (a,b,d) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab4 ORDER BY a");
+is($result, qq(1|red|oh my
+2|blue|hello), 'insert on column tab4.c is not replicated');
+
+# test_part: (a,b) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM test_part ORDER BY a");
+is($result, qq(1|abc
+2|bcd
+7|abc
+8|bcd), 'insert on column test_part.c columns is not replicated');
+
+
+# TEST: now insert more data into the tables, and wait until we replicate
+# them (not by tablesync, but regular decoding and replication)
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab1 VALUES (2, 3, 4);
+	INSERT INTO tab1 VALUES (5, 6, 7);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab3 VALUES (2, 3, 4);
+	INSERT INTO tab3 VALUES (5, 6, 7);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab4 VALUES (3, 'red', 5, 'foo');
+	INSERT INTO tab4 VALUES (4, 'blue', 6, 'bar');
+));
+
+# replication of partitioned table
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part VALUES (3, 'xxx', '2022-02-01 10:00:00');
+	INSERT INTO test_part VALUES (4, 'yyy', '2022-03-02 15:12:13');
+	INSERT INTO test_part VALUES (9, 'zzz', '2022-04-03 21:00:00');
+	INSERT INTO test_part VALUES (10, 'qqq', '2022-05-04 22:12:13');
+));
+
+# wait for catchup before checking the subscriber
+$node_publisher->wait_for_catchup('sub1');
+
+# tab1: only (a,b) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab1 ORDER BY a");
+is($result, qq(1|2|
+2|3|
+4|5|
+5|6|), 'insert on column tab1.c is not replicated');
+
+# tab3: only (a,c) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab3 ORDER BY "a'"));
+is($result, qq(1|3
+2|4
+4|6
+5|7), 'insert on column tab3.b is not replicated');
+
+# tab4: only (a,b,d) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab4 ORDER BY a");
+is($result, qq(1|red|oh my
+2|blue|hello
+3|red|foo
+4|blue|bar), 'insert on column tab4.c is not replicated');
+
+# test_part: (a,b) is replicated
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM test_part ORDER BY a");
+is($result, qq(1|abc
+2|bcd
+3|xxx
+4|yyy
+7|abc
+8|bcd
+9|zzz
+10|qqq), 'insert on column test_part.c columns is not replicated');
+
+
+# TEST: do some updates on some of the tables, both on columns included
+# in the column list and other
+
+# tab1: update of replicated column
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab1 SET "B" = 2 * "B" where a = 1));
+
+# tab1: update of non-replicated column
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab1 SET c = 2*c where a = 4));
+
+# tab3: update of non-replicated
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab3 SET "B" = "B" || ' updated' where "a'" = 4));
+
+# tab3: update of replicated column
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab3 SET "c'" = 2 * "c'" where "a'" = 1));
+
+# tab4
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab4 SET b = 'blue', c = c * 2, d = d || ' updated' where a = 1));
+
+# tab4
+$node_publisher->safe_psql('postgres',
+	qq(UPDATE tab4 SET b = 'red', c = c * 2, d = d || ' updated' where a = 2));
+
+# wait for the replication to catch up, and check the UPDATE results got
+# replicated correctly, with the right column list
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab1 ORDER BY a));
+is($result,
+qq(1|4|
+2|3|
+4|5|
+5|6|), 'only update on column tab1.b is replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab3 ORDER BY "a'"));
+is($result,
+qq(1|6
+2|4
+4|6
+5|7), 'only update on column tab3.c is replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	qq(SELECT * FROM tab4 ORDER BY a));
+
+is($result, qq(1|blue|oh my updated
+2|red|hello updated
+3|red|foo
+4|blue|bar), 'update on column tab4.c is not replicated');
+
+
+# TEST: add table with a column list, insert data, replicate
+
+# insert some data before adding it to the publication
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab2 VALUES (1, 'abc', 3);
+));
+
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION pub1 ADD TABLE tab2 (a, b)");
+
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION");
+
+# wait for the tablesync to complete, add a bit more data and then check
+# the results of the replication
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab2 VALUES (2, 'def', 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab2 ORDER BY a");
+is($result, qq(1|abc
+2|def), 'insert on column tab2.c is not replicated');
+
+# do a couple updates, check the correct stuff gets replicated
+$node_publisher->safe_psql('postgres', qq(
+	UPDATE tab2 SET c = 5 where a = 1;
+	UPDATE tab2 SET b = 'xyz' where a = 2;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab2 ORDER BY a");
+is($result, qq(1|abc
+2|xyz), 'update on column tab2.c is not replicated');
+
+
+# TEST: add a table to two publications with different column lists, and
+# create a single subscription replicating both publications
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab5 (a int PRIMARY KEY, b int, c int, d int);
+	CREATE PUBLICATION pub2 FOR TABLE tab5 (a, b);
+	CREATE PUBLICATION pub3 FOR TABLE tab5 (a, d);
+
+	-- insert a couple initial records
+	INSERT INTO tab5 VALUES (1, 11, 111, 1111);
+	INSERT INTO tab5 VALUES (2, 22, 222, 2222);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab5 (a int PRIMARY KEY, b int, d int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub2, pub3
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->wait_for_catchup('sub1');
+
+# insert data and make sure all the columns (union of the columns lists)
+# get fully replicated
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab5 VALUES (3, 33, 333, 3333);
+	INSERT INTO tab5 VALUES (4, 44, 444, 4444);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab5 ORDER BY a"),
+   qq(1|11|1111
+2|22|2222
+3|33|3333
+4|44|4444),
+   'overlapping publications with overlapping column lists');
+
+# and finally, set the column list to ALL for one of the publications,
+# which means replicating all columns (removing the column list), but
+# first add the missing column to the table on subscriber
+$node_publisher->safe_psql('postgres', qq(
+	ALTER PUBLICATION pub3 ALTER TABLE tab5 SET COLUMNS ALL;
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
+	ALTER TABLE tab5 ADD COLUMN c INT;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab5 VALUES (5, 55, 555, 5555);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab5 ORDER BY a"),
+   qq(1|11|1111|
+2|22|2222|
+3|33|3333|
+4|44|4444|
+5|55|5555|555),
+   'overlapping publications with overlapping column lists');
+
+# TEST: create a table with a column list, then change the replica
+# identity by replacing a primary key (but use a different column in
+# the column list)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab6 (a int PRIMARY KEY, b int, c int, d int);
+	CREATE PUBLICATION pub4 FOR TABLE tab6 (a, b);
+
+	-- initial data
+	INSERT INTO tab6 VALUES (1, 22, 333, 4444);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab6 (a int PRIMARY KEY, b int, c int, d int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub4
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab6 VALUES (2, 33, 444, 5555);
+	UPDATE tab6 SET b = b * 2, c = c * 3, d = d * 4;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab6 ORDER BY a"),
+	qq(1|44||
+2|66||), 'replication with the original primary key');
+
+# now redefine the constraint - move the primary key to a different column
+# (which is still covered by the column list, though)
+
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab6 DROP CONSTRAINT tab6_pkey;
+	ALTER TABLE tab6 ADD PRIMARY KEY (b);
+));
+
+# we need to do the same thing on the subscriber
+# XXX What would happen if this happens before the publisher ALTER? Or
+# interleaved, somehow? But that seems unrelated to column lists.
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER TABLE tab6 DROP CONSTRAINT tab6_pkey;
+	ALTER TABLE tab6 ADD PRIMARY KEY (b);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab6 VALUES (3, 55, 666, 8888);
+	UPDATE tab6 SET b = b * 2, c = c * 3, d = d * 4;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab6 ORDER BY a"),
+   qq(1|88||
+2|132||
+3|110||),
+   'replication with the modified primary key');
+
+
+# TEST: create a table with a column list, then change the replica
+# identity by replacing a primary key with a key on multiple columns
+# (all of them covered by the column list)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE tab7 (a int PRIMARY KEY, b int, c int, d int);
+	CREATE PUBLICATION pub5 FOR TABLE tab7 (a, b);
+
+	-- some initial data
+	INSERT INTO tab7 VALUES (1, 22, 333, 4444);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE tab7 (a int PRIMARY KEY, b int, c int, d int);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub5
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab7 VALUES (2, 33, 444, 5555);
+	UPDATE tab7 SET b = b * 2, c = c * 3, d = d * 4;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab7 ORDER BY a"),
+   qq(1|44||
+2|66||), 'replication with the original primary key');
+
+# now redefine the constraint - move the primary key to a different column
+# (which is not covered by the column list)
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab7 DROP CONSTRAINT tab7_pkey;
+	ALTER TABLE tab7 ADD PRIMARY KEY (a, b);
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO tab7 VALUES (3, 55, 666, 7777);
+	UPDATE tab7 SET b = b * 2, c = c * 3, d = d * 4;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab7 ORDER BY a"),
+   qq(1|88||
+2|132||
+3|110||),
+   'replication with the modified primary key');
+
+# now switch the primary key again to another columns not covered by the
+# column list, but also generate writes between the drop and creation
+# of the new constraint
+
+$node_publisher->safe_psql('postgres', qq(
+	ALTER TABLE tab7 DROP CONSTRAINT tab7_pkey;
+	INSERT INTO tab7 VALUES (4, 77, 888, 9999);
+	-- update/delete is not allowed for tables without RI
+	ALTER TABLE tab7 ADD PRIMARY KEY (b, a);
+	UPDATE tab7 SET b = b * 2, c = c * 3, d = d * 4;
+	DELETE FROM tab7 WHERE a = 1;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab7 ORDER BY a"),
+   qq(2|264||
+3|220||
+4|154||),
+   'replication with the modified primary key');
+
+
+# TEST: partitioned tables (with publish_via_partition_root = false)
+# and replica identity. The (leaf) partitions may have different RI, so
+# we need to check the partition RI (with respect to the column list)
+# while attaching the partition.
+
+# First, let's create a partitioned table with two partitions, each with
+# a different RI, but a column list not covering all those RI.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_a (a int, b int, c int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_a_1 PARTITION OF test_part_a FOR VALUES IN (1,2,3,4,5);
+	ALTER TABLE test_part_a_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_a_1 REPLICA IDENTITY USING INDEX test_part_a_1_pkey;
+
+	CREATE TABLE test_part_a_2 PARTITION OF test_part_a FOR VALUES IN (6,7,8,9,10);
+	ALTER TABLE test_part_a_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_a_2 REPLICA IDENTITY USING INDEX test_part_a_2_pkey;
+
+	-- initial data, one row in each partition
+	INSERT INTO test_part_a VALUES (1, 3);
+	INSERT INTO test_part_a VALUES (6, 4);
+));
+
+# do the same thing on the subscriber (with the opposite column order)
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_a (b int, a int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_a_1 PARTITION OF test_part_a FOR VALUES IN (1,2,3,4,5);
+	ALTER TABLE test_part_a_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_a_1 REPLICA IDENTITY USING INDEX test_part_a_1_pkey;
+
+	CREATE TABLE test_part_a_2 PARTITION OF test_part_a FOR VALUES IN (6,7,8,9,10);
+	ALTER TABLE test_part_a_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_a_2 REPLICA IDENTITY USING INDEX test_part_a_2_pkey;
+));
+
+# create a publication replicating just the column "a", which is not enough
+# for the second partition
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub6 FOR TABLE test_part_a (b, a) WITH (publish_via_partition_root = true);
+	ALTER PUBLICATION pub6 ADD TABLE test_part_a_1 (a);
+	ALTER PUBLICATION pub6 ADD TABLE test_part_a_2 (b);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub6
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_a VALUES (2, 5);
+	INSERT INTO test_part_a VALUES (7, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT a, b FROM test_part_a ORDER BY a, b"),
+   qq(1|3
+2|5
+6|4
+7|6),
+   'partitions with different replica identities not replicated correctly');
+
+# This time start with a column list covering RI for all partitions, but
+# then update the column list to not cover column "b" (needed by the
+# second partition)
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_b (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_b_1 PARTITION OF test_part_b FOR VALUES IN (1,2,3,4,5);
+	ALTER TABLE test_part_b_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_b_1 REPLICA IDENTITY USING INDEX test_part_b_1_pkey;
+
+	CREATE TABLE test_part_b_2 PARTITION OF test_part_b FOR VALUES IN (6,7,8,9,10);
+	ALTER TABLE test_part_b_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_b_2 REPLICA IDENTITY USING INDEX test_part_b_2_pkey;
+
+	-- initial data, one row in each partitions
+	INSERT INTO test_part_b VALUES (1, 1);
+	INSERT INTO test_part_b VALUES (6, 2);
+));
+
+# do the same thing on the subscriber
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_b (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_b_1 PARTITION OF test_part_b FOR VALUES IN (1,2,3,4,5);
+	ALTER TABLE test_part_b_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_b_1 REPLICA IDENTITY USING INDEX test_part_b_1_pkey;
+
+	CREATE TABLE test_part_b_2 PARTITION OF test_part_b FOR VALUES IN (6,7,8,9,10);
+	ALTER TABLE test_part_b_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_b_2 REPLICA IDENTITY USING INDEX test_part_b_2_pkey;
+));
+
+# create a publication replicating both columns, which is sufficient for
+# both partitions
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub7 FOR TABLE test_part_b (a, b) WITH (publish_via_partition_root = true);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub7
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_b VALUES (2, 3);
+	INSERT INTO test_part_b VALUES (7, 4);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_b ORDER BY a, b"),
+   qq(1|1
+2|3
+6|2
+7|4),
+   'partitions with different replica identities not replicated correctly');
+
+
+# TEST: This time start with a column list covering RI for all partitions,
+# but then update RI for one of the partitions to not be covered by the
+# column list anymore.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_c (a int, b int, c int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_c_1 PARTITION OF test_part_c FOR VALUES IN (1,3);
+	ALTER TABLE test_part_c_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_c_1 REPLICA IDENTITY USING INDEX test_part_c_1_pkey;
+
+	CREATE TABLE test_part_c_2 PARTITION OF test_part_c FOR VALUES IN (2,4);
+	ALTER TABLE test_part_c_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_c_2 REPLICA IDENTITY USING INDEX test_part_c_2_pkey;
+
+	-- initial data, one row for each partition
+	INSERT INTO test_part_c VALUES (1, 3, 5);
+	INSERT INTO test_part_c VALUES (2, 4, 6);
+));
+
+# do the same thing on the subscriber
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_c (a int, b int, c int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_c_1 PARTITION OF test_part_c FOR VALUES IN (1,3);
+	ALTER TABLE test_part_c_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_c_1 REPLICA IDENTITY USING INDEX test_part_c_1_pkey;
+
+	CREATE TABLE test_part_c_2 PARTITION OF test_part_c FOR VALUES IN (2,4);
+	ALTER TABLE test_part_c_2 ADD PRIMARY KEY (b);
+	ALTER TABLE test_part_c_2 REPLICA IDENTITY USING INDEX test_part_c_2_pkey;
+));
+
+# create a publication replicating data through partition root, with a column
+# filter on the root, and then add the partitions one by one with separate
+# column lists (but those are not applied)
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub8 FOR TABLE test_part_c WITH (publish_via_partition_root = false);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_1 (a,c);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_2 (a,b);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	DROP SUBSCRIPTION sub1;
+	CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub8;
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_c VALUES (3, 7, 8);
+	INSERT INTO test_part_c VALUES (4, 9, 10);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_c ORDER BY a, b"),
+   qq(1||5
+2|4|
+3||8
+4|9|),
+   'partitions with different replica identities not replicated correctly');
+
+
+# create a publication not replicating data through partition root, without
+# a column list on the root, and then add the partitions one by one with
+# separate column lists
+$node_publisher->safe_psql('postgres', qq(
+	DROP PUBLICATION pub8;
+	CREATE PUBLICATION pub8 FOR TABLE test_part_c WITH (publish_via_partition_root = false);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_1 (a);
+	ALTER PUBLICATION pub8 ADD TABLE test_part_c_2 (a,b);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
+	TRUNCATE test_part_c;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	TRUNCATE test_part_c;
+	INSERT INTO test_part_c VALUES (1, 3, 5);
+	INSERT INTO test_part_c VALUES (2, 4, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_c ORDER BY a, b"),
+   qq(1||
+2|4|),
+   'partitions with different replica identities not replicated correctly');
+
+
+# TEST: Start with a single partition, with RI compatible with the column
+# filter, and then attach a partition with incompatible RI.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_part_d (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_d_1 PARTITION OF test_part_d FOR VALUES IN (1,3);
+	ALTER TABLE test_part_d_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_d_1 REPLICA IDENTITY USING INDEX test_part_d_1_pkey;
+
+	INSERT INTO test_part_d VALUES (1, 2);
+));
+
+# do the same thing on the subscriber (in fact, create both partitions right
+# away, no need to delay that)
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_part_d (a int, b int) PARTITION BY LIST (a);
+
+	CREATE TABLE test_part_d_1 PARTITION OF test_part_d FOR VALUES IN (1,3);
+	ALTER TABLE test_part_d_1 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_d_1 REPLICA IDENTITY USING INDEX test_part_d_1_pkey;
+
+	CREATE TABLE test_part_d_2 PARTITION OF test_part_d FOR VALUES IN (2,4);
+	ALTER TABLE test_part_d_2 ADD PRIMARY KEY (a);
+	ALTER TABLE test_part_d_2 REPLICA IDENTITY USING INDEX test_part_d_2_pkey;
+));
+
+# create a publication replicating both columns, which is sufficient for
+# both partitions
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION pub9 FOR TABLE test_part_d (a) WITH (publish_via_partition_root = true);
+));
+
+# add the publication to our subscription, wait for sync to complete
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub9
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_part_d VALUES (3, 4);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_part_d ORDER BY a, b"),
+   qq(1|
+3|),
+   'partitions with different replica identities not replicated correctly');
+
+# TEST: With a table included in multiple publications, we should use a
+# union of the column lists. So with column lists (a,b) and (a,c) we
+# should replicate (a,b,c).
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c int);
+	CREATE PUBLICATION pub_mix_1 FOR TABLE test_mix_1 (a, b);
+	CREATE PUBLICATION pub_mix_2 FOR TABLE test_mix_1 (a, c);
+
+	-- initial data
+	INSERT INTO test_mix_1 VALUES (1, 2, 3);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c int);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_mix_1, pub_mix_2;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_mix_1 VALUES (4, 5, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_1 ORDER BY a"),
+   qq(1|2|3
+4|5|6),
+   'a mix of publications should use a union of column list');
+
+
+# TEST: With a table included in multiple publications, we should use a
+# union of the column lists. If any of the publications is FOR ALL
+# TABLES, we should replicate all columns.
+
+# drop unnecessary tables, so as not to interfere with the FOR ALL TABLES
+$node_publisher->safe_psql('postgres', qq(
+	DROP TABLE tab1, tab2, tab3, tab4, tab5, tab6, tab7, test_mix_1,
+			   test_part, test_part_a, test_part_b, test_part_c, test_part_d;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_2 (a int PRIMARY KEY, b int, c int);
+	CREATE PUBLICATION pub_mix_3 FOR TABLE test_mix_2 (a, b);
+	CREATE PUBLICATION pub_mix_4 FOR ALL TABLES;
+
+	-- initial data
+	INSERT INTO test_mix_2 VALUES (1, 2, 3);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_2 (a int PRIMARY KEY, b int, c int);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_mix_3, pub_mix_4;
+	ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_mix_2 VALUES (4, 5, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_2"),
+   qq(1|2|3
+4|5|6),
+   'a mix of publications should use a union of column list');
+
+
+# TEST: With a table included in multiple publications, we should use a
+# union of the column lists. If any of the publications is FOR ALL
+# TABLES IN SCHEMA, we should replicate all columns.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_3 (a int PRIMARY KEY, b int, c int);
+	CREATE PUBLICATION pub_mix_5 FOR TABLE test_mix_3 (a, b);
+	CREATE PUBLICATION pub_mix_6 FOR ALL TABLES IN SCHEMA public;
+
+	-- initial data
+	INSERT INTO test_mix_3 VALUES (1, 2, 3);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_mix_3 (a int PRIMARY KEY, b int, c int);
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_mix_5, pub_mix_6;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_mix_3 VALUES (4, 5, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_mix_3"),
+   qq(1|2|3
+4|5|6),
+   'a mix of publications should use a union of column list');
+
+
+# TEST: Check handling of publish_via_partition_root - if a partition is
+# published through partition root, we should only apply the column list
+# defined for the whole table (not the partitions) - both during the initial
+# sync and when replicating changes. This is what we do for row filters.
+
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE test_root (a int PRIMARY KEY, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE test_root_1 PARTITION OF test_root FOR VALUES FROM (1) TO (10);
+	CREATE TABLE test_root_2 PARTITION OF test_root FOR VALUES FROM (10) TO (20);
+
+	CREATE PUBLICATION pub_root_true FOR TABLE test_root (a) WITH (publish_via_partition_root = true);
+
+	-- initial data
+	INSERT INTO test_root VALUES (1, 2, 3);
+	INSERT INTO test_root VALUES (10, 20, 30);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE test_root (a int PRIMARY KEY, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE test_root_1 PARTITION OF test_root FOR VALUES FROM (1) TO (10);
+	CREATE TABLE test_root_2 PARTITION OF test_root FOR VALUES FROM (10) TO (20);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub_root_true;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO test_root VALUES (2, 3, 4);
+	INSERT INTO test_root VALUES (11, 21, 31);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM test_root ORDER BY a, b, c"),
+   qq(1||
+2||
+10||
+11||),
+   'publication via partition root applies column list');
+
+
+# TEST: Multiple publications which publish schema of parent table and
+# partition. The partition is published through two publications, once
+# through a schema (so no column list) containing the parent, and then
+# also directly (with a columns list). The expected outcome is there is
+# no column list.
+
+$node_publisher->safe_psql('postgres', qq(
+	DROP PUBLICATION pub1, pub2, pub3, pub4, pub5, pub6, pub7, pub8;
+
+	CREATE SCHEMA s1;
+	CREATE TABLE s1.t (a int, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE t_1 PARTITION OF s1.t FOR VALUES FROM (1) TO (10);
+
+	CREATE PUBLICATION pub1 FOR ALL TABLES IN SCHEMA s1;
+	CREATE PUBLICATION pub2 FOR TABLE t_1(b);
+
+	-- initial data
+	INSERT INTO s1.t VALUES (1, 2, 3);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE SCHEMA s1;
+	CREATE TABLE s1.t (a int, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE t_1 PARTITION OF s1.t FOR VALUES FROM (1) TO (10);
+
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub1, pub2;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO s1.t VALUES (4, 5, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM s1.t ORDER BY a"),
+   qq(1|2|3
+4|5|6),
+   'two publications, publishing the same relation');
+
+# Now resync the subcription, but with publications in the opposite order.
+# The result should be the same.
+
+$node_subscriber->safe_psql('postgres', qq(
+	TRUNCATE s1.t;
+
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub2, pub1;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO s1.t VALUES (7, 8, 9);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM s1.t ORDER BY a"),
+   qq(7|8|9),
+   'two publications, publishing the same relation');
+
+
+# TEST: One publication, containing both the parent and child relations.
+# The expected outcome is list "a", because that's the column list defined
+# for the top-most ancestor added to the publication.
+
+$node_publisher->safe_psql('postgres', qq(
+	DROP SCHEMA s1 CASCADE;
+	CREATE TABLE t (a int, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE t_1 PARTITION OF t FOR VALUES FROM (1) TO (10)
+		   PARTITION BY RANGE (a);
+	CREATE TABLE t_2 PARTITION OF t_1 FOR VALUES FROM (1) TO (10);
+
+	CREATE PUBLICATION pub3 FOR TABLE t_1 (a), t_2
+	  WITH (PUBLISH_VIA_PARTITION_ROOT);
+
+	-- initial data
+	INSERT INTO t VALUES (1, 2, 3);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	DROP SCHEMA s1 CASCADE;
+	CREATE TABLE t (a int, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE t_1 PARTITION OF t FOR VALUES FROM (1) TO (10)
+		   PARTITION BY RANGE (a);
+	CREATE TABLE t_2 PARTITION OF t_1 FOR VALUES FROM (1) TO (10);
+
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub3;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO t VALUES (4, 5, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM t ORDER BY a, b, c"),
+   qq(1||
+4||),
+   'publication containing both parent and child relation');
+
+
+# TEST: One publication, containing both the parent and child relations.
+# The expected outcome is list "a", because that's the column list defined
+# for the top-most ancestor added to the publication.
+# Note: The difference from the preceding test is that in this case both
+# relations have a column list defined.
+
+$node_publisher->safe_psql('postgres', qq(
+	DROP TABLE t;
+	CREATE TABLE t (a int, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE t_1 PARTITION OF t FOR VALUES FROM (1) TO (10)
+		   PARTITION BY RANGE (a);
+	CREATE TABLE t_2 PARTITION OF t_1 FOR VALUES FROM (1) TO (10);
+
+	CREATE PUBLICATION pub4 FOR TABLE t_1 (a), t_2 (b)
+	  WITH (PUBLISH_VIA_PARTITION_ROOT);
+
+	-- initial data
+	INSERT INTO t VALUES (1, 2, 3);
+));
+
+$node_subscriber->safe_psql('postgres', qq(
+	DROP TABLE t;
+	CREATE TABLE t (a int, b int, c int) PARTITION BY RANGE (a);
+	CREATE TABLE t_1 PARTITION OF t FOR VALUES FROM (1) TO (10)
+		   PARTITION BY RANGE (a);
+	CREATE TABLE t_2 PARTITION OF t_1 FOR VALUES FROM (1) TO (10);
+
+	ALTER SUBSCRIPTION sub1 SET PUBLICATION pub4;
+));
+
+wait_for_subscription_sync($node_subscriber);
+
+$node_publisher->safe_psql('postgres', qq(
+	INSERT INTO t VALUES (4, 5, 6);
+));
+
+$node_publisher->wait_for_catchup('sub1');
+
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM t ORDER BY a, b, c"),
+   qq(1||
+4||),
+   'publication containing both parent and child relation');
+
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 10 Mar 2022 19:20:10 +0100

On 3/9/22 10:20, Peter Eisentraut wrote:
>  Also, there seemed to be a bit of copy-and-pasting from row-filter code
> going on, with some code comments not quite sensible, so I rewrote some
> of them.  Also some code used "rf" and "cf" symbols which were a bit
> hard to tell apart.  A few more letters can increase readability.
> 

Thanks, I've merged these changes into the patch.

> subvert those checks by altering the replica identity later.

Are you sure? Which part of the patch does that? AFAICS we only do those
checks in CheckCmdReplicaIdentity now, but maybe I'm missing something.
Are you sure you're not looking at some older patch version?

regards

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 10 Mar 2022 20:10:01 +0100

> .txt, so as not to confuse cfbot).

FWIW I think the reason is pretty simple - pgoutput_row_filter_init is
broken. It assumes you can just do this

rftuple = SearchSysCache2(PUBLICATIONRELMAP,
                          ObjectIdGetDatum(entry->publish_as_relid),
                          ObjectIdGetDatum(pub->oid));

if (HeapTupleIsValid(rftuple))
{
    /* Null indicates no filter. */
    rfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
                              Anum_pg_publication_rel_prqual,
                              &pub_no_filter);
}
else
{
    pub_no_filter = true;
}

and pub_no_filter=true means there's no filter at all. Which is
nonsense, because we're using publish_as_relid here - the publication
may not include this particular ancestor, in which case we need to just
ignore this publication.

So yeah, this needs to be reworked.

regards

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 11 Mar 2022 02:56:41 +0100

> So yeah, this needs to be reworked.

I spent a bit of time looking at this, and I think a minor change in
get_rel_sync_entry() fixes this - it's enough to ensure rel_publications
only includes publications that actually include publish_as_relid.

But this does not address tablesync.c :-( That still copies everything,
because it decides to sync both rels (test_pub_part_1, test_pub_part_2),
with it's row filter. On older releases this would fail, because we'd
start two workers:

1) COPY public.test_part_rf_2 TO STDOUT

2) COPY (SELECT a, b, c FROM public.test_part_rf_1) TO STDOUT

And that ends up inserting date from test_part_rf_2 twice. But now we
end up doing this instead:

1) COPY (SELECT a, b, c FROM public.test_part_rf_1 WHERE (b < 10)) TO STDOUT

2) COPY (SELECT a, b, c FROM ONLY public.test_part_rf_2 WHERE (b > 10))
TO STDOUT

Which no longer conflicts, because those subsets are mutually exclusive
(due to how the filter is defined), so the sync succeeds.

But I find this really weird - I think it's reasonable to expect the
sync to produce the same result as if the data was inserted and
replicated, and this just violates that.

Shouldn't tablesync calculate a list of relations in a way that prevents
such duplicate / overlapping syncs? In any case, this sync issue looks
entirely unrelated to the column filtering patch.

regards

From 0726a46f050980df2ebbca48078b4fd3b0d374f8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 9 Mar 2022 18:10:56 +0100
Subject: [PATCH 1/3] fixup: publish_as_relid

Make sure to determine the top-most ancestor listed in any publication.
Otherwise we might end up with different values depending on the order
of publications (as listed in subscription).
---
 src/backend/replication/pgoutput/pgoutput.c | 32 +++++++++++++++++++--
 1 file changed, 30 insertions(+), 2 deletions(-)

+			/*
 
 			if (!publish)

 
-- 
2.34.1

From 09b3cb5368b2fd3448384c8cf8860801d7328479 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Fri, 11 Mar 2022 02:23:25 +0100
Subject: [PATCH 2/3] fixup: row-filter publications

When initializing the row filter, consider only publications that
actually include the relation (publish_as_relid). The publications may
include different ancestors, in which case the function would get
confused and conclude there's no row filter.
---
 src/backend/replication/pgoutput/pgoutput.c | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index dbac2690b7f..faf7555f913 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1875,8 +1875,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				entry->pubactions.pubdelete |= pub->pubactions.pubdelete;
 				entry->pubactions.pubtruncate |= pub->pubactions.pubtruncate;
 
-				rel_publications = lappend(rel_publications, pub);
-
 				/*
 				 * We want to publish the changes as the top-most ancestor
 				 * across all publications. So we fetch all ancestors of the
@@ -1894,8 +1892,18 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				if (list_member_oid(ancestors, publish_as_relid))
 					continue;
 
-				/* The new value is an ancestor, so let's keep it. */
-				publish_as_relid = pub_relid;
+				/*
+				 * If the new value is an ancestor, discard the list of
+				 * publications through which we replicate it.
+				 */
+				if (publish_as_relid != pub_relid)
+				{
+					rel_publications = NIL;
+					publish_as_relid = pub_relid;
+				}
+
+				/* Track the publications. */
+				rel_publications = lappend(rel_publications, pub);
 			}
 		}
 
-- 
2.34.1

From 66d45f6a5865c67d2377e2db2afd247cf6842fde Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Thu, 10 Mar 2022 17:31:57 +0100
Subject: [PATCH 3/3] Allow specifying column filters for logical replication

 

+  <para>
 
   <para>
 
   <para>

 
+     <para>

 
 
+	/*

 
+/*
 
+/*
 
+/*
 
+
+/*

+/*
 
 	/*

+			has_row_filter

 

+	/*

 

 
+			/*

 
 			foreach(newlc, rels)
 			{

 				/*
 
 /*
 
 	/*

 		if (t->whereClause)
 
+		if (t->columns)
 
+					/*

 				if (t->whereClause)
 
+		if (pubrel->columns)
+			ereport(ERROR,
 
 	ReleaseSysCache(tuple);
 
+	/*
 
 	if (children)
 	{

 

 
 
 	return newnode;
 }
 
 	return true;
 }

 
 
+
+static bool

 
 /*

 
 /*

 /*

 
 /*

 
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;

+	/*

index faf7555f913..e50af5017f9 100644

 

 
 	if (in_streaming)

 
 
+/*

 
 		/*
 
 			/*
@@ -1921,6 +2054,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)

 		list_free(pubids);

 
+		/*
 
 		/*

 

 

 /*

 

 

 

 
 
 /*

---

From: Amit Kapila <[email-masked]>
Date: Fri, 11 Mar 2022 09:00:31 +0530

> with it's row filter. On older releases this would fail, because we'd

Yeah, this is because of the existing problem where we sync both rels
instead of one. We have fixed some similar existing problems earlier.
Hou-San has reported a similar case in another email [1].

> such duplicate / overlapping syncs?
>

Yes, I think it is better to fix it separately than to fix it along
with row filter or column filter work.

In any case, this sync issue looks

Right.

[1] - https://www.postgresql.org/message-id/OS0PR01MB5716DC2982CC735FDE388804940B9%40OS0PR01MB5716.jpnprd01.prod.outlook.com

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Fri, 11 Mar 2022 15:22:19 +0530

> only includes publications that actually include publish_as_relid.

Thanks for looking into this. I think in the first patch before
calling get_partition_ancestors() we need to ensure it is a partition
(the call expects that) and pubviaroot is true. I think it would be
good if we can avoid an additional call to get_partition_ancestors()
as it could be costly. I wonder why it is not sufficient to ensure
that publish_as_relid exists after ancestor in ancestors list before
assigning the ancestor to publish_as_relid? This only needs to be done
in case of (if (!publish)). I have not tried this so I could be wrong.

-- 
With Regards,
Amit Kapila.

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 11 Mar 2022 13:50:29 +0100

On 3/11/22 10:52, Amit Kapila wrote:
> (the call expects that) and pubviaroot is true.

Does the call really require that? Also, I'm not sure why we'd need to
look at pubviaroot - that's already considered earlier when calculating
publish_as_relid, here we just need to know the relationship of the two
OIDs (if one is ancestor/child of the other).

> I think it would be
> as it could be costly.

Maybe. OTOH we only should do this only very rarely anyway.

> I wonder why it is not sufficient to ensure

I'm not sure what exactly are you proposing. Maybe try coding it? That's
probably faster than trying to describe what the code might do ...

regards