Subject: Column Filtering in Logical Replication

From: Rahila Syed <[email-masked]>
Date: Thu, 1 Jul 2021 01:06:11 +0530

Hi,

Filtering of columns at the publisher node will allow for selective
replication of data between publisher and subscriber.  In case the updates
on the publisher are targeted only towards specific columns, the user will
have an option to reduce network consumption by not sending the data
corresponding to new columns that do not change. Note that replica
identity values will always be sent irrespective of column filtering settings.
The column values that are not sent by the publisher will be populated
using local values on the subscriber. For insert command, non-replicated
column values will be NULL or the default.
If column names are not specified while creating or altering a publication,
all the columns are replicated as per current behaviour.

The proposal for syntax to add table with column names to publication is as
follows:
Create publication:

CREATE PUBLICATION <pub_name> [ FOR TABLE [ONLY] table_name [(colname
[,…])] | FOR ALL TABLES]

Alter publication:

ALTER PUBLICATION <pub_name> ADD TABLE [ONLY] table_name [(colname [, ..])]

Please find attached a patch that implements the above proposal.
While the patch contains basic implementation and tests, several
improvements
and sanity checks are underway. I will post an updated patch with those
changes soon.

Kindly let me know your opinion.

Thank you,

Rahila Syed

---

From: Dilip Kumar <[email-masked]>
Date: Thu, 1 Jul 2021 11:50:36 +0530

On Thu, Jul 1, 2021 at 1:06 AM Rahila Syed <[email-masked]> wrote:
>
> Hi,
>
> Filtering of columns at the publisher node will allow for selective replication of data between publisher and subscriber.  In case the updates on the publisher are targeted only towards specific columns, the user will have an option to reduce network consumption by not sending the data corresponding to new columns that do not change. Note that replica identity values will always be sent irrespective of column filtering settings. The column values that are not sent by the publisher will be populated using local values on the subscriber. For insert command, non-replicated column values will be NULL or the default.
> The proposal for syntax to add table with column names to publication is as follows:
> Create publication:
>
> CREATE PUBLICATION <pub_name> [ FOR TABLE [ONLY] table_name [(colname [,…])] | FOR ALL TABLES]
>
>
> Alter publication:
>
> While the patch contains basic implementation and tests, several improvements
> and sanity checks are underway. I will post an updated patch with those changes soon.
>

I haven't looked into the patch yet but +1 for the idea.

-- 
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

---

From: vignesh C <[email-masked]>
Date: Thu, 1 Jul 2021 18:58:58 +0530

> Kindly let me know your opinion.

This idea gives more flexibility to the user, +1 for the feature.

Regards,
Vignesh

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 6 Jul 2021 19:42:51 -0400

Hello, here are a few comments on this patch.

The patch adds a function get_att_num_by_name; but we have a lsyscache.c
function for that purpose, get_attnum.  Maybe that one should be used
instead?

get_tuple_columns_map() returns a bitmapset of the attnos of the columns
in the given list, so its name feels wrong.  I propose
get_table_columnset().  However, this function is invoked for every
insert/update change, so it's going to be far too slow to be usable.  I
think you need to cache the bitmapset somewhere, so that the function is
only called on first use.  I didn't look very closely, but it seems that
struct RelationSyncEntry may be a good place to cache it.

The patch adds a new parse node PublicationTable, but doesn't add
copyfuncs.c, equalfuncs.c, readfuncs.c, outfuncs.c support for it.
Maybe try a compile with WRITE_READ_PARSE_PLAN_TREES and/or
COPY_PARSE_PLAN_TREES enabled to make sure everything is covered.
(I didn't verify that this actually catches anything ...)

The new column in pg_publication_rel is prrel_attr.  This name seems at
odds with existing column names (we don't use underscores in catalog
column names).  Maybe prattrs is good enough?  prrelattrs?  We tend to
use plurals for columns that are arrays.

It's not super clear to me that strlist_to_textarray() and related
processing will behave sanely when the column names contain weird
characters such as commas or quotes, or just when used with uppercase
column names.  Maybe it's worth having tests that try to break such
cases.

You seem to have left a debugging "elog(LOG)" line in OpenTableList.

I got warnings from "git am" about trailing whitespace being added by
the patch in two places.

Thanks!

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/

---

From: Peter Smith <[email-masked]>
Date: Thu, 8 Jul 2021 13:27:27 +1000

Hi, I was wondering if/when a subset of cols is specified then does
that mean it will be possible for the table to be replicated to a
*smaller* table at the subscriber side?

e.g Can a table with 7 cols replicated to a table with 2 cols?

table tab1(a,b,c,d,e,f,g) --> CREATE PUBLICATION pub1 FOR TABLE
tab1(a,b)  --> table tab1(a,b)

~~

I thought maybe that should be possible, but the expected behaviour
for that scenario was not very clear to me from the thread/patch
comments. And the new TAP test uses the tab1 table created exactly the
same for pub/sub, so I couldn't tell from the test code either.

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Rahila Syed <[email-masked]>
Date: Mon, 12 Jul 2021 14:02:26 +0530

Hi Peter,

> same for pub/sub, so I couldn't tell from the test code either.

Currently, this capability is not included in the patch. If the table on
the subscriber
server has lesser attributes than that on the publisher server, it throws
an error at the
time of CREATE SUBSCRIPTION.

About having such a functionality, I don't immediately see any issue with
it as long
as we make sure replica identity columns are always present on both
instances.
However, need to carefully consider situations in which a server subscribes
to multiple
publications,  each publishing a different subset of columns of a table.

Thank you,
Rahila Syed

---

From: Rahila Syed <[email-masked]>
Date: Mon, 12 Jul 2021 15:08:23 +0530

Hi Alvaro,

Thank you for comments.

> Thank you for pointing that out, I agree it makes sense to reuse the
existing function.
Changed it accordingly in the attached patch.

> Makes sense, changed accordingly.

> (I didn't verify that this actually catches anything ...)

I will test this and include these changes in the next version.

> Renamed it to prattrs as per suggestion.

> Sure, I will include these tests in the next version.

> Removed.

> Should be fixed now.

Thank you,
Rahila Syed

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 12 Jul 2021 14:42:55 +0200

On 7/12/21 10:32 AM, Rahila Syed wrote:
> server has lesser attributes than that on the publisher server, it
> throws an error at the 

That's a bit surprising, to be honest. I do understand the patch simply
treats the filtered columns as "unchanged" because that's the simplest
way to filter the *data* of the columns. But if someone told me we can
"filter columns" I'd expect this to work without the columns on the
subscriber.

> About having such a functionality, I don't immediately see any issue
> with it as long

Yeah, that seems like an inherent requirement.

> However, need to carefully consider situations in which a server
> subscribes to multiple 

Isn't that pretty much the same situation as for multiple subscriptions
each with a different set of I/U/D operations? IIRC we simply merge
those, so why not to do the same thing here and merge the attributes?

regards

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

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 12 Jul 2021 14:54:26 +0200

> Makes sense, changed accordingly.

To nitpick, I find "Bitmapset *att_list" a bit annoying, because it's
not really a list ;-)

FWIW "make check" fails for me with this version, due to segfault in
OpenTableLists. Apparenly there's some confusion - the code expects the
list to contain PublicationTable nodes, and tries to extract the
RangeVar from the elements. But the list actually contains RangeVar, so
this crashes and burns. See the attached backtrace.

I'd bet this is because the patch uses list of RangeVar in some cases
and list of PublicationTable in some cases, similarly to the "row
filtering" patch nearby. IMHO this is just confusing and we should
always pass list of PublicationTable nodes.

regards

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 12 Jul 2021 10:53:01 -0400

> always pass list of PublicationTable nodes.

+1 don't make the code guess what type of list it is.  Changing all the
uses of that node to deal with PublicationTable seems best.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Cuando no hay humildad las personas se degradan" (A. Christie)

---

From: Rahila Syed <[email-masked]>
Date: Tue, 13 Jul 2021 20:13:44 +0530

Hi Tomas,

Thank you for your comments.

> OK, I will look into adding this.

> those, so why not to do the same thing here and merge the attributes?
Yeah, I agree with the solution to merge the attributes, similar to how
operations are merged. My concern was also from an implementation point
of view, will it be a very drastic change. I now had a look at how remote
relation
attributes are acquired for comparison with local attributes at the
subscriber.
It seems that the publisher will need to send the information about the
filtered columns
for each publication specified during CREATE SUBSCRIPTION.
This will be read at the subscriber side which in turn updates its cache
accordingly.
Currently, the subscriber expects all attributes of a published relation to
be present.
I will add code for this in the next version of the patch.

not really a list ;-)

I will make this change with the next version

> this crashes and burns. See the attached backtrace.
Thank you for the report, This is fixed in the attached version, now all
publication
function calls accept the PublicationTableInfo list.

Thank you,
Rahila Syed

---

From: Ibrar Ahmed <[email-masked]>
Date: Mon, 19 Jul 2021 15:20:32 +0500

On Tue, Jul 13, 2021 at 7:44 PM Rahila Syed <[email-masked]> wrote:

> Currently, the subscriber expects all attributes of a published relation
> to be present.

The patch does not apply, and an rebase is required

Hunk #8 succeeded at 1259 (offset 99 lines).
Hunk #9 succeeded at 1360 (offset 99 lines).
1 out of 9 hunks FAILED -- saving rejects to file
src/backend/replication/pgoutput/pgoutput.c.rej
patching file src/include/catalog/pg_publication.h

Changing the status to "Waiting on Author"

-- 
Ibrar Ahmed

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 20 Jul 2021 20:14:17 -0400

Hello,

I think this looks good regarding the PublicationRelationInfo API that was
discussed.

Looking at OpenTableList(), I think you forgot to update the comment --
it says "open relations specified by a RangeVar list", but the list is
now of PublicationTable.  Also I think it would be good to say that the
returned tables are PublicationRelationInfo, maybe such as "In the
returned list of PublicationRelationInfo, the tables are locked ..."

In AlterPublicationTables() I was confused by some code that seemed
commented a bit too verbosely (for a moment I thought the whole list was
being copied into a different format).  May I suggest something more
compact like

			/* Not yet in list; open it and add it to the list */
			if (!found)
			{
				Relation	oldrel;
				PublicationRelationInfo *pubrel;
			   
				oldrel = table_open(oldrelid, ShareUpdateExclusiveLock);

				/* Wrap it in PublicationRelationInfo */
				pubrel = palloc(sizeof(PublicationRelationInfo));
				pubrel->relation = oldrel;
				pubrel->relid = oldrelid;
				pubrel->columns = NIL;		/* not needed */

				delrels = lappend(delrels, pubrel);
			}

Thanks!

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 22 Jul 2021 14:48:32 -0400

One thing I just happened to notice is this part of your commit message

: REPLICA IDENTITY columns are always replicated
: irrespective of column names specification.

... for which you don't have any tests -- I mean, create a table with a
certain REPLICA IDENTITY and later try to publish a set of columns that
doesn't include all the columns in the replica identity, then verify
that those columns are indeed published.

Having said that, I'm not sure I agree with this design decision; what I
think this is doing is hiding from the user the fact that they are
publishing columns that they don't want to publish.  I think as a user I
would rather get an error in that case:

  ERROR:  invalid column list in published set
  DETAIL:  The set of published commands does not include all the replica identity columns.

or something like that.  Avoid possible nasty surprises of security-
leaking nature.

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/
"On the other flipper, one wrong move and we're Fatal Exceptions"
(T.U.X.: Term Unit X  - http://www.thelinuxreview.com/TUX/)

---

From: Rahila Syed <[email-masked]>
Date: Mon, 9 Aug 2021 01:29:50 +0530

Hi,

>> OK, I will look into adding this.

This has been added in the attached patch. Now, instead of
treating the filtered columns as unchanged and sending a byte
with that information, unfiltered columns are not sent to the subscriber
server at all. This along with saving the network bandwidth, allows
the logical replication to even work between tables with different numbers
of
columns i.e with the table on subscriber server containing only the
filtered
columns. Currently, replica identity columns are replicated irrespective of
the presence of the column filters, hence the table on the subscriber side
must
contain the replica identity columns.

> copyfuncs.c, equalfuncs.c, readfuncs.c, outfuncs.c support for it.

Thanks, added this.

> it says "open relations specified by a RangeVar list",

Thank you for the review, Modified this.

> not really a list ;-)

Changed this.

> column names.  Maybe it's worth having tests that try to break such

Added a few test cases for this.

> commented a bit too verbosely

Modified this as per the suggestion.

> : irrespective of column names specification.

... for which you don't have any tests

I have added these tests.

> leaking nature.

Ok, Thank you for your opinion. I agree that giving an explicit error in
this case will be safer.
I will include this, in case there are no counter views.

Thank you for your review comments. Please find attached the rebased and
updated patch.

Thank you,
Rahila Syed

---

From: Amit Kapila <[email-masked]>
Date: Mon, 9 Aug 2021 15:59:16 +0530

On Mon, Aug 9, 2021 at 1:36 AM Rahila Syed <[email-masked]> wrote:
> Ok, Thank you for your opinion. I agree that giving an explicit error in this case will be safer.
>

+1 for an explicit error in this case.

Can you please explain why you have the restriction for including
replica identity columns and do we want to put a similar restriction
for the primary key? As far as I understand, if we allow default
values on subscribers for replica identity, then probably updates,
deletes won't work as they need to use replica identity (or PK) to
search the required tuple. If so, shouldn't we add this restriction
only when a publication has been defined for one of these (Update,
Delete) actions?

Another point is what if someone drops the column used in one of the
publications? Do we want to drop the entire relation from publication
or just remove the column filter or something else?

Do we want to consider that the columns specified in the filter must
not have NOT NULL constraint? Because, otherwise, the subscriber will
error out inserting such rows?

Minor comments:
================
  pq_sendbyte(out, flags);
-
  /* attribute name */
  pq_sendstring(out, NameStr(att->attname));

@@ -953,6 +1000,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)

  /* attribute mode */
  pq_sendint32(out, att->atttypmod);
+
  }

  bms_free(idattrs);
diff --git a/src/backend/replication/logical/relation.c
b/src/backend/replication/logical/relation.c
index c37e2a7e29..d7a7b00841 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -354,7 +354,6 @@ logicalrep_rel_open(LogicalRepRelId remoteid,
LOCKMODE lockmode)

  attnum = logicalrep_rel_att_by_name(remoterel,
  NameStr(attr->attname));
-
  entry->attrmap->attnums[i] = attnum;

There are quite a few places in the patch that contains spurious line
additions or removals.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Mon, 9 Aug 2021 16:15:32 +0530

> error out inserting such rows?

I noticed that other databases provide this feature [1] and they allow
users to specify "Columns that are included in Filter" or specify "All
columns to be included in filter except for a subset of columns". I am
not sure if want to provide both ways in the first version but at
least we should consider it as a future extensibility requirement and
try to choose syntax accordingly.

[1] - https://docs.oracle.com/en/cloud/paas/goldengate-cloud/gwuad/selecting-columns.html#GUID-9A851C8B-48F7-43DF-8D98-D086BE069E20

-- 
With Regards,
Amit Kapila.

---

From: Rahila Syed <[email-masked]>
Date: Thu, 12 Aug 2021 08:40:00 +0530

Hi Amit,

Thanks for your review.

> only when a publication has been defined for one of these (Update,

Yes, like you mentioned they are needed for Updates and Deletes to work.
The restriction for including replica identity columns in column filters
exists because
In case the replica identity column values did not change, the old row
replica identity columns
are not sent to the subscriber, thus we would need new replica identity
columns
to be sent to identify the row that is to be Updated or Deleted.
I haven't tested if it would break Insert as well  though. I will update
the patch accordingly.

> or just remove the column filter or something else?
Thanks for pointing this out. Currently, this is not handled in the patch.
I think dropping the column from the filter would make sense on the lines
of the table being dropped from publication, in case of drop table.

> I think you mean columns *not* specified in the filter must not have NOT
NULL constraint
on the subscriber, as this will break during insert, as it will try to
insert NULL for columns
not sent by the publisher.
I will look into fixing this. Probably this won't be a problem in
case the column is auto generated or contains a default value.

> Minor comments:
Thank you for your comments, I will fix these.

Thank you,
Rahila Syed

---

From: Amit Kapila <[email-masked]>
Date: Thu, 12 Aug 2021 13:59:27 +0530

On Thu, Aug 12, 2021 at 8:40 AM Rahila Syed <[email-masked]> wrote:
> The restriction for including replica identity columns in column filters exists because
> In case the replica identity column values did not change, the old row replica identity columns
> are not sent to the subscriber, thus we would need new replica identity columns
> I haven't tested if it would break Insert as well  though. I will update the patch accordingly.
>

Okay, but then we also need to ensure that the user shouldn't be
allowed to enable the 'update' or 'delete' for a publication that
contains some filter that doesn't have replica identity columns.

> of the table being dropped from publication, in case of drop table.

I think it would be tricky if you want to remove the column from the
filter because you need to recompute the entire filter and update it
again. Also, you might need to do this for all the publications that
have a particular column in their filter clause. It might be easier to
drop the entire filter but you can check if it is easier another way
than it is good.

> I think you mean columns *not* specified in the filter must not have NOT NULL constraint
> on the subscriber, as this will break during insert, as it will try to insert NULL for columns

Right.

-- 
With Regards,
Amit Kapila.

---

From: Rahila Syed <[email-masked]>
Date: Thu, 2 Sep 2021 02:51:41 +0530

Hi,

>> or just remove the column filter or something else?
After thinking about this, I think it is best to remove the entire table
from publication,
if a column specified in the column filter is dropped from the table.
Because, if we drop the entire filter without dropping the table, it means
all the columns will be replicated,
and the downstream server table might not have those columns.
If we drop only the column from the filter we might have to recreate the
filter and check for replica identity.
That means if the replica identity column is dropped, you can't drop it
from the filter,
and might have to drop the entire publication-table mapping anyways.

Thus, I think it is cleanest to drop the entire relation from publication.

This has been implemented in the attached version.

> case the column is auto generated or contains a default value.
I am not sure if this needs to be handled. Ideally, we need to prevent the
subscriber tables from having a NOT NULL
constraint if the publisher uses column filters to publish the values of
the table. There is no way
to do this at the time of creating a table on subscriber.
As this involves querying the publisher for this information, it can be
done at the time of initial table synchronization.
i.e error out if any of the subscribed tables has NOT NULL constraint on
non-filter columns.
This will lead to the user dropping and recreating the subscription after
removing the
NOT NULL constraint from the table.
I think the same can be achieved by doing nothing and letting the
subscriber error out while inserting rows.

Minor comments:
> Fixed these in the attached patch.

>   DETAIL:  The set of published commands does not include all the replica

Added this.

Also added some more tests. Please find attached a rebased and updated
patch.

Thank you,
Rahila Syed

---

From: Peter Smith <[email-masked]>
Date: Thu, 2 Sep 2021 17:53:59 +1000

On Thu, Sep 2, 2021 at 7:21 AM Rahila Syed <[email-masked]> wrote:
...
>
> Also added some more tests. Please find attached a rebased and updated patch.

I fetched and applied the v4 patch.

It applied cleanly, and the build and make check was OK.

But I encountered some errors running the TAP subscription tests, as follows:

...
t/018_stream_subxact_abort.pl ...... ok
t/019_stream_subxact_ddl_abort.pl .. ok
t/020_messages.pl .................. ok
t/021_column_filter.pl ............. 1/9
#   Failed test 'insert on column c is not replicated'
#   at t/021_column_filter.pl line 126.
#          got: ''
#     expected: '1|abc'

#   Failed test 'update on column c is not replicated'
#   at t/021_column_filter.pl line 130.
#          got: ''
#     expected: '1|abc'
# Looks like you failed 2 tests of 9.
t/021_column_filter.pl ............. Dubious, test returned 2 (wstat 512, 0x200)
Failed 2/9 subtests
t/021_twophase.pl .................. ok
t/022_twophase_cascade.pl .......... ok
t/023_twophase_stream.pl ........... ok
t/024_add_drop_pub.pl .............. ok
t/100_bugs.pl ...................... ok

Test Summary Report
-------------------
t/021_column_filter.pl           (Wstat: 512 Tests: 9 Failed: 2)
  Failed tests:  6-7
  Non-zero exit status: 2
Files=26, Tests=263, 192 wallclock secs ( 0.57 usr  0.09 sys + 110.17
cusr 25.45 csys = 136.28 CPU)
Result: FAIL
make: *** [check] Error 1

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 2 Sep 2021 04:49:08 -0400

> if a column specified in the column filter is dropped from the table.

Hmm, I think it would be cleanest to give responsibility to the user: if
the column to be dropped is in the filter, then raise an error, aborting
the drop.  Then it is up to them to figure out what to do.

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
"El destino baraja y nosotros jugamos" (A. Schopenhauer)

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 2 Sep 2021 04:56:39 -0400

I think the WITH RECURSIVE query would be easier and more performant by
using pg_partition_tree and pg_partition_root.

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
"Porque Kim no hacía nada, pero, eso sí,
con extraordinario éxito" ("Kim", Kipling)

---

From: Amit Kapila <[email-masked]>
Date: Sat, 4 Sep 2021 09:41:30 +0530

On Thu, Sep 2, 2021 at 2:19 PM Alvaro Herrera <[email-masked]> wrote:
> the drop.

Do you think that will make sense if the user used Cascade (Alter
Table ... Drop Column ... Cascade)?

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Sat, 4 Sep 2021 10:12:53 +0530

On Thu, Sep 2, 2021 at 2:51 AM Rahila Syed <[email-masked]> wrote:
> I am not sure if this needs to be handled. Ideally, we need to prevent the subscriber tables from having a NOT NULL
> constraint if the publisher uses column filters to publish the values of the table. There is no way
> As this involves querying the publisher for this information, it can be done at the time of initial table synchronization.
> i.e error out if any of the subscribed tables has NOT NULL constraint on non-filter columns.
> This will lead to the user dropping and recreating the subscription after removing the
> I think the same can be achieved by doing nothing and letting the subscriber error out while inserting rows.
>

That makes sense and also it is quite possible that users don't have
such columns in the tables on subscribers. I guess we can add such a
recommendation in the docs instead of doing anything in the code.

Few comments:
============
1.
+
+ /*
+ * Cannot specify column filter when REPLICA IDENTITY IS FULL
+ * or if column filter does not contain REPLICA IDENITY columns
+ */
+ if (targetcols != NIL)
+ {
+ if (replidentfull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ RelationGetRelationName(targetrel)),
+ errdetail("Cannot have column filter with REPLICA IDENTITY FULL")));

Why do we want to have such a restriction for REPLICA IDENTITY FULL? I
think it is better to expand comments in that regards.

2.
@@ -839,7 +839,6 @@ NextCopyFrom(CopyFromState cstate, ExprContext *econtext,
  ereport(ERROR,
  (errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
  errmsg("extra data after last expected column")));
-
  fieldno = 0;

@@ -944,7 +992,6 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
  flags |= LOGICALREP_IS_REPLICA_IDENTITY;

  pq_sendbyte(out, flags);
-
  /* attribute name */

  /* attribute mode */

Spurious line removals and addition.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Sat, 4 Sep 2021 10:30:17 +0530

> recommendation in the docs instead of doing anything in the code.

Did you give any thoughts to my earlier suggestion related to syntax [1]?

[1] - https://www.postgresql.org/message-id/CAA4eK1J9b_0_PMnJ2jq9E55bcbmTKdUmy6jPnkf1Zwy2jxah_g%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Alvaro Herrera <[email-masked]>
Date: Sat, 4 Sep 2021 10:41:04 -0400

> Table ... Drop Column ... Cascade)?

... ugh.  Since CASCADE is already defined to be a potentially-data-loss
operation, then that may be acceptable behavior.  For sure the default
RESTRICT behavior shouldn't do it, though.

---

From: Amit Kapila <[email-masked]>
Date: Mon, 6 Sep 2021 08:53:39 +0530

> RESTRICT behavior shouldn't do it, though.

That makes sense to me.

-- 
With Regards,
Amit Kapila.

---

From: Rahila Syed <[email-masked]>
Date: Mon, 6 Sep 2021 21:25:42 +0530

Hi,

On Mon, Sep 6, 2021 at 8:53 AM Amit Kapila <[email-masked]> wrote:

> However, the default (RESTRICT) behaviour of DROP TABLE allows
removing the table from the publication. I have implemented the removal of
table from publication
on drop column (RESTRICT)  on the same lines.

Although it does make sense to not allow dropping tables from publication,
in case of RESTRICT.
It makes me wonder how DROP TABLE (RESTRICT) allows cascading the drop
table to publication.

> [1] -
> https://www.postgresql.org/message-id/CAA4eK1J9b_0_PMnJ2jq9E55bcbmTKdUmy6jPnkf1Zwy2jxah_g%40mail.gmail.com

For future support to replicate all columns except (x,y,z), I think some
optional keywords like
COLUMNS NOT IN can be inserted between table name and (*columns_list*) as
follows.
ALTER PUBLICATION ADD TABLE tab_name [COLUMNS NOT IN] (x,y,z)
I think this should be possible as a future addition to proposed syntax in
the patch.
Please let me know your opinion.

Thank you,
Rahila Syed

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 6 Sep 2021 14:51:52 -0300

On 2021-Sep-06, Rahila Syed wrote:

> > > ... ugh.  Since CASCADE is already defined to be a
> > > potentially-data-loss operation, then that may be acceptable
> > > behavior.  For sure the default RESTRICT behavior shouldn't do it,
> > > though.
> >
> > That makes sense to me.
>
> removing the table from the publication. I have implemented the
> removal of table from publication on drop column (RESTRICT)  on the
> same lines.

But dropping the table is quite a different action from dropping a
column, isn't it?  If you drop a table, it seems perfectly reasonable
that it has to be removed from the publication -- essentially, when the
user drops a table, she is saying "I don't care about this table
anymore".  However, if you drop just one column, that doesn't
necessarily mean that the user wants to stop publishing the whole table.
Removing the table from the publication in ALTER TABLE DROP COLUMN seems
like an overreaction.  (Except perhaps in the special case were the
column being dropped is the only one that was being published.)

So let's discuss what should happen.  If you drop a column, and the
column is filtered out, then it seems to me that the publication should
continue to have the table, and it should continue to filter out the
other columns that were being filtered out, regardless of CASCADE/RESTRICT.
However, if the column is *included* in the publication, and you drop
it, ISTM there are two cases:

1. If it's DROP CASCADE, then the list of columns to replicate should
continue to have all columns it previously had, so just remove the
column that is being dropped.

2. If it's DROP RESTRICT, then an error should be raised so that the
user can make a concious decision to remove the column from the filter
before dropping the column.

> [1] https://www.postgresql.org/message-id/CAA4eK1J9b_0_PMnJ2jq9E55bcbmTKdUmy6jPnkf1Zwy2jxah_g%40mail.gmail.com

This is a great followup idea, after the current feature is committed.
There are a few things that have been reported in review comments; let's
get those addressed before adding more features on top.

I pushed the clerical part of this -- namely the addition of
PublicationTable node and PublicationRelInfo struct.  I attach the part
of your v4 patch that I didn't include.  It contains a couple of small
corrections, but I didn't do anything invasive (such as pgindent)
because that would perhaps cause you too much merge pain.

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 6 Sep 2021 16:16:03 -0300

The code in get_rel_sync_entry() changes current memory context to
CacheMemoryContext, then does a bunch of memory-leaking things.  This is
not good, because that memory context has to be very carefully managed
to avoid permanent memory leaks.  I suppose you added that because you
need something -- probably entry->att_map -- to survive memory context
resets, but if so then you need to change to CacheMemoryContext only
when that memory is allocated, not other chunks of memory.  I suspect
you can fix this by moving the MemoryContextSwitchTo() to just before
calling get_table_columnset; then all the leaky thinkgs are done in
whatever the original memory context is, which is fine.

(However, you also need to make sure that ->att_map is carefully freed
at the right time.  It looks like this already happens in
rel_sync_cache_relation_cb, but is rel_sync_cache_publication_cb
correct?  And in get_rel_sync_entry() itself, what if the entry already
has att_map -- should it be freed prior to allocating another one?)

By the way, I notice that your patch doesn't add documentation changes,
which are of course necessary.

/me is left wondering about PGOutputData->publication_names memory
handling ...

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 6 Sep 2021 23:03:50 +0200

> before dropping the column.

FWIW I think this is a sensible behavior.

I don't quite see why dropping a column should remove the table from
publication (assuming there are some columns still replicated).

Of course, it may break the subscriber (e.g. when there was NOT NULL
constraint on that column), but DROP RESTRICT (which I assume is the
default mode) prevents that. And if DROP CASCADE is specified, I think
it's reasonable to require the user to fix the fallout.

regards

---

From: Peter Smith <[email-masked]>
Date: Tue, 7 Sep 2021 10:29:49 +1000

Here are some v5 review comments for your consideration:

------

1. src/backend/access/common/relation.c

@@ -215,3 +217,22 @@ relation_close(Relation relation, LOCKMODE lockmode)
  if (lockmode != NoLock)
  UnlockRelationId(&relid, lockmode);
 }
+
+/*
+ * Return a bitmapset of attributes given the list of column names
+ */
+Bitmapset*
+get_table_columnset(Oid relid, List *columns, Bitmapset *att_map)
+{

IIUC that 3rd parameter (att_map) is always passed as NULL to
get_table_columnset function because you are constructing this
Bitmapset from scratch. Maybe I am mistaken, but if not then what is
the purpose of that att_map parameter?

------

2. src/backend/catalog/pg_publication.c

+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ RelationGetRelationName(targetrel)),
+ errdetail("Column filter must include REPLICA IDENTITY columns")));

Is ERRCODE_INVALID_COLUMN_REFERENCE a more appropriate errcode to use here?

------

3. src/backend/catalog/pg_publication.c

+ else
+ {
+ Bitmapset *filtermap = NULL;
+ idattrs = RelationGetIndexAttrBitmap(targetrel,
INDEX_ATTR_BITMAP_IDENTITY_KEY);

The RelationGetIndexAttrBitmap function comment says "should be
bms_free'd when not needed anymore" but it seems the patch code is not
freeing idattrs when finished using it.

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Amit Kapila <[email-masked]>
Date: Tue, 7 Sep 2021 10:30:18 +0530

On Mon, Sep 6, 2021 at 9:25 PM Rahila Syed <[email-masked]> wrote:
> For future support to replicate all columns except (x,y,z), I think some optional keywords like
> COLUMNS NOT IN can be inserted between table name and (*columns_list*) as follows.
> I think this should be possible as a future addition to proposed syntax in the patch.

Right, I don't want you to implement that feature as part of this
patch but how about using COLUMNS or similar keyword in column filter
like ALTER PUBLICATION ADD TABLE tab_name COLUMNS (c1, c2, ...)? This
can make it easier to extend in the future.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Tue, 7 Sep 2021 11:05:54 +0530

> other columns that were being filtered out, regardless of CASCADE/RESTRICT.

Yeah, for this case we don't need to do anything and I am not sure if
the patch is dropping tables in this case?

> column that is being dropped.

Note that for a somewhat similar case in the index (where the index
has an expression) we drop the index if one of the columns used in the
index expression is dropped, so we might want to just remove the
entire filter here instead of just removing the particular column or
remove the entire table from publication as Rahila is proposing.

I think removing just a particular column can break the replication
for Updates and Deletes if the removed column is part of replica
identity. If the entire filter is removed then also the entire
replication can break, so, I think Rahila's proposal is worth
considering.

> before dropping the column.

I think one can argue for a similar case for index. If we are allowing
the index to be dropped even with RESTRICT then why not column filter?

> This is a great followup idea, after the current feature is committed.

As mentioned in my response to Rahila, I was just thinking of using an
optional keyword Column for column filter so that we can extend it
later.

-- 
With Regards,
Amit Kapila.

---

From: Dilip Kumar <[email-masked]>
Date: Tue, 7 Sep 2021 11:26:17 +0530

On Tue, Sep 7, 2021 at 11:06 AM Amit Kapila <[email-masked]> wrote:

> identity.

But how this is specific to this patch, I think the behavior should be the
same as what is there now, I mean now also we can drop the columns which
are part of replica identity right.

---

From: Amit Kapila <[email-masked]>
Date: Tue, 7 Sep 2021 12:13:08 +0530

On Tue, Sep 7, 2021 at 11:26 AM Dilip Kumar <[email-masked]> wrote:
> But how this is specific to this patch, I think the behavior should be the same as what is there now, I mean now also we can drop the columns which are part of replica identity right.
>

Sure, but we drop replica identity and corresponding index as well.
The patch ensures that replica identity columns must be part of the
column filter and now that restriction won't hold anymore. I think if
we want to retain that restriction then it is better to either remove
the entire filter or remove the entire table. Anyway, the main point
was that if we can remove the index/replica identity, it seems like
there should be the same treatment for column filter.

Another related point that occurred to me is that if the user changes
replica identity then probably we should ensure that the column
filters for the table still holds the creteria or maybe we need to
remove the filter in that case as well. I am not sure if the patch is
already doing something about it and if not then isn't it better to do
something about it?

-- 
With Regards,
Amit Kapila.

---

From: "Euler Taveira" <[email-masked]>
Date: Tue, 07 Sep 2021 19:39:30 -0300

> because that would perhaps cause you too much merge pain.
While updating the row filter patch [1] (because it also uses these
structures), I noticed that you use PublicationRelInfo as a type name instead
of PublicationRelationInfo. I choose the latter because there is already a data
structure named PublicationRelInfo (pg_dump.h). It is a client-side data
structure but I doesn't seem a good practice to duplicate data structure names
over the same code base.

[1] https://www.postgresql.org/message-id/0c2464d4-65f4-4d91-aeb2-c5584c1350f5%40www.fastmail.com

--
Euler Taveira
EDB   https://www.enterprisedb.com/

---

From: Amit Kapila <[email-masked]>
Date: Wed, 15 Sep 2021 09:04:21 +0530

On Mon, Sep 6, 2021 at 11:21 PM Alvaro Herrera <[email-masked]> wrote:
> PublicationTable node and PublicationRelInfo struct.
>

One point to note here is that we are developing a generic grammar for
publications where not only tables but other objects like schema,
sequences, etc. can be specified, see [1]. So, there is some overlap
in the grammar modifications being made by this patch and the work
being done in that other thread. As both the patches are being
developed at the same time, it might be better to be in sync,
otherwise, some of the work needs to be changed. I can see that in the
patch [2] (v28-0002-Added-schema-level-support-for-publication) being
developed there the changes made by the above commit needs to be
changed again to represent a generic object for publication. It is
possible that we can do it some other way but I think it would be
better to coordinate the work in both threads. The other approach is
to continue independently and the later patch can adapt to the earlier
one which is fine too but it might be more work for the later one.

[1] - https://www.postgresql.org/message-id/877603.1629120678%40sss.pgh.pa.us
[2] - postgresql.org/message-id/CALDaNm0OudeDeFN7bSWPro0hgKx%3D1zPgcNFWnvU_G6w3mDPX0Q%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 15 Sep 2021 08:50:07 -0300

On 2021-Sep-15, Amit Kapila wrote:

> being done in that other thread.

Oh rats.  I was not aware of that thread, or indeed of the fact that
adding multiple object types to publications was being considered.

I do see that 0002 there contains gram.y changes, but AFAICS those
changes don't allow specifying a column list for a table, so there are
some changes needed in that patch for that either way.

I agree that it's better to move forward in unison.

I noticed that 0002 in that other patch uses a void * pointer in
PublicationObjSpec that "could be either RangeVar or String", which
strikes me as a really bad idea.  (Already discussed in some other
thread recently, maybe this one or the row filtering one.)

---

From: vignesh C <[email-masked]>
Date: Wed, 15 Sep 2021 17:49:19 +0530

> thread recently, maybe this one or the row filtering one.)

I have extracted the parser code and attached it here, so that it will
be easy to go through. We wanted to support the following syntax as in
[1]:
CREATE PUBLICATION pub1 FOR
TABLE t1,t2,t3, ALL TABLES IN SCHEMA s1,s2,
SEQUENCE seq1,seq2, ALL SEQUENCES IN SCHEMA s3,s4;

Columns can be added to PublicationObjSpec data structure. The patch
Generic_object_type_parser_002_table_schema_publication.patch has the
changes that were used to handle the parsing. Schema and Relation both
are different objects, schema is of string type and relation is of
RangeVar type. While parsing, schema name is parsed in string format
and relation is parsed and converted to rangevar type, these objects
will be then handled accordingly during post processing. That is the
reason it used void * type which could hold both RangeVar and String.
Thoughts?

Regards,
Vignesh
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6fddd6efe..2a2fe03c13 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -141,14 +141,14 @@ pg_relation_is_publishable(PG_FUNCTION_ARGS)
  * Insert new publication / relation mapping.
  */
 ObjectAddress
-publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
+publication_add_relation(Oid pubid, Relation targetrel,
 						 bool if_not_exists)
 {
 	Relation	rel;
 	HeapTuple	tup;
 	Datum		values[Natts_pg_publication_rel];
 	bool		nulls[Natts_pg_publication_rel];
-	Oid			relid = RelationGetRelid(targetrel->relation);
+	Oid			relid = RelationGetRelid(targetrel);
 	Oid			prrelid;
 	Publication *pub = GetPublication(pubid);
 	ObjectAddress myself,
@@ -172,10 +172,10 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 		ereport(ERROR,
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("relation \"%s\" is already member of publication \"%s\"",
-						RelationGetRelationName(targetrel->relation), pub->name)));
+						RelationGetRelationName(targetrel), pub->name)));
 	}
 
-	check_publication_add_relation(targetrel->relation);
+	check_publication_add_relation(targetrel);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
@@ -209,7 +209,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	table_close(rel, RowExclusiveLock);
 
 	/* Invalidate relcache so that publication info is rebuilt. */
-	CacheInvalidateRelcache(targetrel->relation);
+	CacheInvalidateRelcache(targetrel);
 
 	return myself;
 }
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 30929da1f5..f5fd463c15 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -34,6 +34,7 @@
 #include "commands/publicationcmds.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -138,6 +139,34 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpecType list into rangevar list.
+ */
+static void
+ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
+						   List **rels)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		Node *node;
+
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+		node = (Node *) pubobj->object;
+
+		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
+		{
+			if (IsA(node, RangeVar))
+				*rels = lappend(*rels, (RangeVar *) node);
+		}		
+	}
+}
+ 
 /*
  * Create new publication.
  */
@@ -155,6 +184,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	bool		publish_via_partition_root_given;
 	bool		publish_via_partition_root;
 	AclResult	aclresult;
+	List	   *relations = NIL;
 
 	/* must have CREATE privilege on database */
 	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
@@ -224,13 +254,14 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	if (stmt->tables)
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations);
+	if (relations != NIL)
 	{
 		List	   *rels;
 
-		Assert(list_length(stmt->tables) > 0);
+		Assert(list_length(relations) > 0);
 
-		rels = OpenTableList(stmt->tables);
+		rels = OpenTableList(relations);
 		PublicationAddTables(puboid, rels, true, NULL);
 		CloseTableList(rels);
 	}
@@ -360,7 +391,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
  */
 static void
 AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
-					   HeapTuple tup)
+					   HeapTuple tup, List *tables)
 {
 	List	   *rels = NIL;
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
@@ -374,13 +405,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
 
-	Assert(list_length(stmt->tables) > 0);
+	Assert(list_length(tables) > 0);
 
-	rels = OpenTableList(stmt->tables);
+	rels = OpenTableList(tables);
 
-	if (stmt->tableAction == DEFELEM_ADD)
+	if (stmt->action == DEFELEM_ADD)
 		PublicationAddTables(pubid, rels, false, stmt);
-	else if (stmt->tableAction == DEFELEM_DROP)
+	else if (stmt->action == DEFELEM_DROP)
 		PublicationDropTables(pubid, rels, false);
 	else						/* DEFELEM_SET */
 	{
@@ -398,10 +429,9 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
 
 			foreach(newlc, rels)
 			{
-				PublicationRelInfo *newpubrel;
+				Relation	newrel = (Relation) lfirst(newlc);
 
-				newpubrel = (PublicationRelInfo *) lfirst(newlc);
-				if (RelationGetRelid(newpubrel->relation) == oldrelid)
+				if (RelationGetRelid(newrel) == oldrelid)
 				{
 					found = true;
 					break;
@@ -410,16 +440,10 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
 			/* Not yet in the list, open it and add to the list */
 			if (!found)
 			{
-				Relation	oldrel;
-				PublicationRelInfo *pubrel;
-
-				/* Wrap relation into PublicationRelInfo */
-				oldrel = table_open(oldrelid, ShareUpdateExclusiveLock);
+				Relation	oldrel = table_open(oldrelid,
+												ShareUpdateExclusiveLock);
 
-				pubrel = palloc(sizeof(PublicationRelInfo));
-				pubrel->relation = oldrel;
-
-				delrels = lappend(delrels, pubrel);
+				delrels = lappend(delrels, oldrel);
 			}
 		}
 
@@ -450,6 +474,7 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 	Relation	rel;
 	HeapTuple	tup;
 	Form_pg_publication pubform;
+	List	   *relations = NIL;
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
@@ -469,10 +494,12 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else
-		AlterPublicationTables(stmt, rel, tup);
+		AlterPublicationTables(stmt, rel, tup, relations);
 
 	/* Cleanup. */
 	heap_freetuple(tup);
@@ -540,7 +567,7 @@ RemovePublicationById(Oid pubid)
 
 /*
  * Open relations specified by a PublicationTable list.
- * In the returned list of PublicationRelInfo, tables are locked
+ * In the returned list of Relation, tables are locked
  * in ShareUpdateExclusiveLock mode in order to add them to a publication.
  */
 static List *
@@ -555,16 +582,15 @@ OpenTableList(List *tables)
 	 */
 	foreach(lc, tables)
 	{
-		PublicationTable *t = lfirst_node(PublicationTable, lc);
-		bool		recurse = t->relation->inh;
+		RangeVar   *rv = lfirst_node(RangeVar, lc);
+		bool		recurse = rv->inh;
 		Relation	rel;
 		Oid			myrelid;
-		PublicationRelInfo *pub_rel;
 
 		/* Allow query cancel in case this takes a long time */
 		CHECK_FOR_INTERRUPTS();
 
-		rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+		rel = table_openrv(rv, ShareUpdateExclusiveLock);
 		myrelid = RelationGetRelid(rel);
 
 		/*
@@ -580,9 +606,7 @@ OpenTableList(List *tables)
 			continue;
 		}
 
-		pub_rel = palloc(sizeof(PublicationRelInfo));
-		pub_rel->relation = rel;
-		rels = lappend(rels, pub_rel);
+		rels = lappend(rels, rel);
 		relids = lappend_oid(relids, myrelid);
 
 		/*
@@ -615,9 +639,7 @@ OpenTableList(List *tables)
 
 				/* find_all_inheritors already got lock */
 				rel = table_open(childrelid, NoLock);
-				pub_rel = palloc(sizeof(PublicationRelInfo));
-				pub_rel->relation = rel;
-				rels = lappend(rels, pub_rel);
+				rels = lappend(rels, rel);
 				relids = lappend_oid(relids, childrelid);
 			}
 		}
@@ -638,10 +660,9 @@ CloseTableList(List *rels)
 
 	foreach(lc, rels)
 	{
-		PublicationRelInfo *pub_rel;
+		Relation	rel = (Relation) lfirst(lc);
 
-		pub_rel = (PublicationRelInfo *) lfirst(lc);
-		table_close(pub_rel->relation, NoLock);
+		table_close(rel, NoLock);
 	}
 }
 
@@ -658,8 +679,7 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 
 	foreach(lc, rels)
 	{
-		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
-		Relation	rel = pub_rel->relation;
+		Relation	rel = (Relation) lfirst(lc);
 		ObjectAddress obj;
 
 		/* Must be owner of the table or superuser. */
@@ -667,7 +687,7 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
 						   RelationGetRelationName(rel));
 
-		obj = publication_add_relation(pubid, pub_rel, if_not_exists);
+		obj = publication_add_relation(pubid, rel, if_not_exists);
 		if (stmt)
 		{
 			EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress,
@@ -691,8 +711,7 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 
 	foreach(lc, rels)
 	{
-		PublicationRelInfo *pubrel = (PublicationRelInfo *) lfirst(lc);
-		Relation	rel = pubrel->relation;
+		Relation	rel = (Relation) lfirst(lc);
 		Oid			relid = RelationGetRelid(rel);
 
 		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 228387eaee..ade93023b8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4817,7 +4817,7 @@ _copyCreatePublicationStmt(const CreatePublicationStmt *from)
 
 	COPY_STRING_FIELD(pubname);
 	COPY_NODE_FIELD(options);
-	COPY_NODE_FIELD(tables);
+	COPY_NODE_FIELD(pubobjects);
 	COPY_SCALAR_FIELD(for_all_tables);
 
 	return newnode;
@@ -4830,9 +4830,9 @@ _copyAlterPublicationStmt(const AlterPublicationStmt *from)
 
 	COPY_STRING_FIELD(pubname);
 	COPY_NODE_FIELD(options);
-	COPY_NODE_FIELD(tables);
+	COPY_NODE_FIELD(pubobjects);
 	COPY_SCALAR_FIELD(for_all_tables);
-	COPY_SCALAR_FIELD(tableAction);
+	COPY_SCALAR_FIELD(action);
 
 	return newnode;
 }
@@ -4958,12 +4958,14 @@ _copyForeignKeyCacheInfo(const ForeignKeyCacheInfo *from)
 	return newnode;
 }
 
-static PublicationTable *
-_copyPublicationTable(const PublicationTable *from)
+static PublicationObjSpec *
+_copyPublicationObject(const PublicationObjSpec *from)
 {
-	PublicationTable *newnode = makeNode(PublicationTable);
+	PublicationObjSpec *newnode = makeNode(PublicationObjSpec);
 
-	COPY_NODE_FIELD(relation);
+	COPY_SCALAR_FIELD(pubobjtype);
+	COPY_NODE_FIELD(object);
+	COPY_LOCATION_FIELD(location);
 
 	return newnode;
 }
@@ -5887,8 +5889,8 @@ copyObjectImpl(const void *from)
 		case T_PartitionCmd:
 			retval = _copyPartitionCmd(from);
 			break;
-		case T_PublicationTable:
-			retval = _copyPublicationTable(from);
+		case T_PublicationObjSpec:
+			retval = _copyPublicationObject(from);
 			break;
 
 			/*
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 800f588b5c..06917598f4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2302,7 +2302,7 @@ _equalCreatePublicationStmt(const CreatePublicationStmt *a,
 {
 	COMPARE_STRING_FIELD(pubname);
 	COMPARE_NODE_FIELD(options);
-	COMPARE_NODE_FIELD(tables);
+	COMPARE_NODE_FIELD(pubobjects);
 	COMPARE_SCALAR_FIELD(for_all_tables);
 
 	return true;
@@ -2314,9 +2314,9 @@ _equalAlterPublicationStmt(const AlterPublicationStmt *a,
 {
 	COMPARE_STRING_FIELD(pubname);
 	COMPARE_NODE_FIELD(options);
-	COMPARE_NODE_FIELD(tables);
+	COMPARE_NODE_FIELD(pubobjects);
 	COMPARE_SCALAR_FIELD(for_all_tables);
-	COMPARE_SCALAR_FIELD(tableAction);
+	COMPARE_SCALAR_FIELD(action);
 
 	return true;
 }
@@ -3134,9 +3134,9 @@ _equalBitString(const BitString *a, const BitString *b)
 }
 
 static bool
-_equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
+_equalPublicationObject(const PublicationObjSpec *a, const PublicationObjSpec *b)
 {
-	COMPARE_NODE_FIELD(relation);
+	COMPARE_NODE_FIELD(object);
 
 	return true;
 }
@@ -3894,8 +3894,8 @@ equal(const void *a, const void *b)
 		case T_PartitionCmd:
 			retval = _equalPartitionCmd(a, b);
 			break;
-		case T_PublicationTable:
-			retval = _equalPublicationTable(a, b);
+		case T_PublicationObjSpec:
+			retval = _equalPublicationObject(a, b);
 			break;
 
 		default:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e3068a374e..d3d7683511 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -256,6 +256,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	PartitionSpec		*partspec;
 	PartitionBoundSpec	*partboundspec;
 	RoleSpec			*rolespec;
+	PublicationObjSpec	*publicationobjectspec;
 	struct SelectLimit	*selectlimit;
 	SetQuantifier	 setquantifier;
 	struct GroupClause  *groupclause;
@@ -425,14 +426,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				transform_element_list transform_type_list
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
-				drop_option_list publication_table_list
+				drop_option_list pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
 %type <list>	group_by_list
 %type <node>	group_by_item empty_grouping_set rollup_clause cube_clause
 %type <node>	grouping_sets_clause
-%type <node>	opt_publication_for_tables publication_for_tables publication_table
 
 %type <list>	opt_fdw_options fdw_options
 %type <defelt>	fdw_option
@@ -554,6 +554,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	var_value zone_value
 %type <rolespec> auth_ident RoleSpec opt_granted_by
 
+%type <publicationobjectspec> PublicationObjSpec
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
@@ -9591,69 +9592,75 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 
 /*****************************************************************************
  *
- * CREATE PUBLICATION name [ FOR TABLE ] [ WITH options ]
+ * CREATE PUBLICATION name [WITH options]
+ *
+ * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ *
+ * CREATE PUBLICATION FOR pub_obj [, pub_obj2] [WITH options]
+ *
+ * pub_obj is one of:
+ *
+ *		TABLE table [, table2]
+ *		ALL TABLES IN SCHEMA schema [, schema2]
  *
  *****************************************************************************/
 
 CreatePublicationStmt:
-			CREATE PUBLICATION name opt_publication_for_tables opt_definition
+			CREATE PUBLICATION name opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $5;
-					if ($4 != NULL)
-					{
-						/* FOR TABLE */
-						if (IsA($4, List))
-							n->tables = (List *)$4;
-						/* FOR ALL TABLES */
-						else
-							n->for_all_tables = true;
-					}
+					n->options = $4;
 					$$ = (Node *)n;
 				}
-		;
-
-opt_publication_for_tables:
-			publication_for_tables					{ $$ = $1; }
-			| /* EMPTY */							{ $$ = NULL; }
-		;
-
-publication_for_tables:
-			FOR TABLE publication_table_list
+			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
 				{
-					$$ = (Node *) $3;
+					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
+					n->pubname = $3;
+					n->options = $7;
+					n->for_all_tables = true;
+					$$ = (Node *)n;
 				}
-			| FOR ALL TABLES
+			| CREATE PUBLICATION name FOR TABLE pub_obj_list opt_definition
 				{
-					$$ = (Node *) makeInteger(true);
+					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
+					n->pubname = $3;
+					n->options = $7;
+					n->pubobjects = (List *)$6;
+					$$ = (Node *)n;
 				}
 		;
 
-publication_table_list:
-			publication_table
-					{ $$ = list_make1($1); }
-		| publication_table_list ',' publication_table
-				{ $$ = lappend($1, $3); }
+/* FOR TABLE and FOR ALL TABLES IN SCHEMA specifications */
+PublicationObjSpec:	relation_expr
+				{
+					PublicationObjSpec *n = makeNode(PublicationObjSpec);
+					n->object = (Node*)$1;
+					n->pubobjtype = PUBLICATIONOBJ_TABLE;
+					$$ = n;
+				}
 		;
 
-publication_table: relation_expr
-		{
-			PublicationTable *n = makeNode(PublicationTable);
-			n->relation = $1;
-			$$ = (Node *) n;
-		}
+pub_obj_list: 	PublicationObjSpec
+					{ $$ = list_make1($1); }
+			| pub_obj_list ',' PublicationObjSpec
+					{ $$ = lappend($1, $3); }
 	;
 
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
  *
- * ALTER PUBLICATION name ADD TABLE table [, table2]
+ * ALTER PUBLICATION name ADD pub_obj [, pub_obj ...]
+ *
+ * ALTER PUBLICATION name DROP pub_obj [, pub_obj ...]
+ *
+ * ALTER PUBLICATION name SET pub_obj [, pub_obj ...]
  *
- * ALTER PUBLICATION name DROP TABLE table [, table2]
+ * pub_obj is one of:
  *
- * ALTER PUBLICATION name SET TABLE table [, table2]
+ *		TABLE table_name [, table_name ...]
+ *		ALL TABLES IN SCHEMA schema_name [, schema_name ...]
  *
  *****************************************************************************/
 
@@ -9665,28 +9672,28 @@ AlterPublicationStmt:
 					n->options = $5;
 					$$ = (Node *)n;
 				}
-			| ALTER PUBLICATION name ADD_P TABLE publication_table_list
+			| ALTER PUBLICATION name ADD_P TABLE pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
-					n->tables = $6;
-					n->tableAction = DEFELEM_ADD;
+					n->pubobjects = $6;
+					n->action = DEFELEM_ADD;
 					$$ = (Node *)n;
 				}
-			| ALTER PUBLICATION name SET TABLE publication_table_list
+			| ALTER PUBLICATION name SET TABLE pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
-					n->tables = $6;
-					n->tableAction = DEFELEM_SET;
+					n->pubobjects = $6;
+					n->action = DEFELEM_SET;
 					$$ = (Node *)n;
 				}
-			| ALTER PUBLICATION name DROP TABLE publication_table_list
+			| ALTER PUBLICATION name DROP TABLE pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
-					n->tables = $6;
-					n->tableAction = DEFELEM_DROP;
+					n->pubobjects = $6;
+					n->action = DEFELEM_DROP;
 					$$ = (Node *)n;
 				}
 		;
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 561266aa3e..f332bad4d4 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -83,11 +83,6 @@ typedef struct Publication
 	PublicationActions pubactions;
 } Publication;
 
-typedef struct PublicationRelInfo
-{
-	Relation	relation;
-} PublicationRelInfo;
-
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
@@ -113,7 +108,7 @@ extern List *GetAllTablesPublications(void);
 extern List *GetAllTablesPublicationRelations(bool pubviaroot);
 
 extern bool is_publishable_relation(Relation rel);
-extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
+extern ObjectAddress publication_add_relation(Oid pubid, Relation targetrel,
 											  bool if_not_exists);
 
 extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index b3ee4194d3..40aadb37d9 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -487,7 +487,7 @@ typedef enum NodeTag
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
 	T_VacuumRelation,
-	T_PublicationTable,
+	T_PublicationObjSpec,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 45e4f2a16e..3e88965316 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -353,6 +353,26 @@ typedef struct RoleSpec
 	int			location;		/* token location, or -1 if unknown */
 } RoleSpec;
 
+/*
+ * Publication object type
+ */
+typedef enum PublicationObjSpecType
+{
+	PUBLICATIONOBJ_TABLE,				/* Table type */
+	PUBLICATIONOBJ_REL_IN_SCHEMA,		/* Relations in schema type */
+	PUBLICATIONOBJ_UNKNOWN				/* Unknown type */
+} PublicationObjSpecType;
+
+typedef struct PublicationObjSpec
+{
+	NodeTag		type;
+	PublicationObjSpecType pubobjtype;	/* type of this publication object */
+	Node	   *object;			/* publication object could be:
+								 * RangeVar - table object
+								 * String	- tablename or schemaname */
+	int			location;		/* token location, or -1 if unknown */
+} PublicationObjSpec;
+
 /*
  * FuncCall - a function or aggregate invocation
  *
@@ -3636,18 +3656,12 @@ typedef struct AlterTSConfigurationStmt
 	bool		missing_ok;		/* for DROP - skip error if missing? */
 } AlterTSConfigurationStmt;
 
-typedef struct PublicationTable
-{
-	NodeTag		type;
-	RangeVar   *relation;		/* relation to be published */
-} PublicationTable;
-
 typedef struct CreatePublicationStmt
 {
 	NodeTag		type;
 	char	   *pubname;		/* Name of the publication */
 	List	   *options;		/* List of DefElem nodes */
-	List	   *tables;			/* Optional list of tables to add */
+	List	   *pubobjects;		/* Optional list of publication objects */
 	bool		for_all_tables; /* Special publication for all tables in db */
 } CreatePublicationStmt;
 
@@ -3659,10 +3673,11 @@ typedef struct AlterPublicationStmt
 	/* parameters used for ALTER PUBLICATION ... WITH */
 	List	   *options;		/* List of DefElem nodes */
 
-	/* parameters used for ALTER PUBLICATION ... ADD/DROP TABLE */
-	List	   *tables;			/* List of tables to add/drop */
+	/* ALTER PUBLICATION ... ADD/DROP TABLE/ALL TABLES IN SCHEMA parameters */
+	List	   *pubobjects;		/* Optional list of publication objects */
 	bool		for_all_tables; /* Special publication for all tables in db */
-	DefElemAction tableAction;	/* What action to perform with the tables */
+	DefElemAction action;		/* What action to perform with the
+								 * tables/schemas */
 } AlterPublicationStmt;
 
 typedef struct CreateSubscriptionStmt
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 423780652f..8a1b97836e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2045,8 +2045,9 @@ PsqlSettings
 Publication
 PublicationActions
 PublicationInfo
+PublicationObjSpec
+PublicationObjSpecType
 PublicationPartOpt
-PublicationRelInfo
 PublicationTable
 PullFilter
 PullFilterOps
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6fddd6efe..2a2fe03c13 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -141,14 +141,14 @@ pg_relation_is_publishable(PG_FUNCTION_ARGS)
  * Insert new publication / relation mapping.
  */
 ObjectAddress
-publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
+publication_add_relation(Oid pubid, Relation targetrel,
 						 bool if_not_exists)
 {
 	Relation	rel;
 	HeapTuple	tup;
 	Datum		values[Natts_pg_publication_rel];
 	bool		nulls[Natts_pg_publication_rel];
-	Oid			relid = RelationGetRelid(targetrel->relation);
+	Oid			relid = RelationGetRelid(targetrel);
 	Oid			prrelid;
 	Publication *pub = GetPublication(pubid);
 	ObjectAddress myself,
@@ -172,10 +172,10 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 		ereport(ERROR,
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("relation \"%s\" is already member of publication \"%s\"",
-						RelationGetRelationName(targetrel->relation), pub->name)));
+						RelationGetRelationName(targetrel), pub->name)));
 	}
 
-	check_publication_add_relation(targetrel->relation);
+	check_publication_add_relation(targetrel);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
@@ -209,7 +209,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	table_close(rel, RowExclusiveLock);
 
 	/* Invalidate relcache so that publication info is rebuilt. */
-	CacheInvalidateRelcache(targetrel->relation);
+	CacheInvalidateRelcache(targetrel);
 
 	return myself;
 }
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 30929da1f5..4e4e02ba70 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -34,6 +34,7 @@
 #include "commands/publicationcmds.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -138,6 +139,85 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpecType list into schema oid list and rangevar
+ * list.
+ */
+static void
+ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
+						   List **rels, List **schemas)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_UNKNOWN;
+
+	if (!pubobjspec_list)
+		return;
+
+	pubobj = (PublicationObjSpec *) linitial(pubobjspec_list);
+	if (pubobj->pubobjtype == PUBLICATIONOBJ_UNKNOWN)
+		ereport(ERROR,
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("FOR TABLE/FOR ALL TABLES IN SCHEMA should be specified before the table/schema name(s)"),
+				parser_errposition(pstate, pubobj->location));
+
+	foreach(cell, pubobjspec_list)
+	{
+		Node *node;
+
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+		node = (Node *) pubobj->object;
+
+		if (pubobj->pubobjtype == PUBLICATIONOBJ_UNKNOWN)
+			pubobj->pubobjtype = prevobjtype;
+		else
+			prevobjtype = pubobj->pubobjtype;
+
+		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
+		{
+			if (IsA(node, RangeVar))
+				*rels = lappend(*rels, (RangeVar *) node);
+			else if (IsA(node, String))
+			{
+				RangeVar   *rel = makeRangeVar(NULL, strVal(node),
+											   pubobj->location);
+				*rels = lappend(*rels, rel);
+			}
+		}
+		else if (pubobj->pubobjtype == PUBLICATIONOBJ_REL_IN_SCHEMA)
+		{
+			Oid			schemaid;
+			char	   *schemaname;
+
+			if (!IsA(node, String))
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("invalid schema name at or near"),
+						parser_errposition(pstate, pubobj->location));
+
+			schemaname = strVal(node);
+			if (strcmp(schemaname, "CURRENT_SCHEMA") == 0)
+			{
+				List	   *search_path;
+
+				search_path = fetch_search_path(false);
+				if (search_path == NIL) /* nothing valid in search_path? */
+					ereport(ERROR,
+							errcode(ERRCODE_UNDEFINED_SCHEMA),
+							errmsg("no schema has been selected for CURRENT_SCHEMA"));
+
+				schemaid = linitial_oid(search_path);
+				list_free(search_path);
+			}
+			else
+				schemaid = get_namespace_oid(schemaname, false);
+
+			/* Filter out duplicates if user specifies "sch1, sch1" */
+			*schemas = list_append_unique_oid(*schemas, schemaid);
+		}
+	}
+}
+ 
 /*
  * Create new publication.
  */
@@ -155,6 +235,8 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	bool		publish_via_partition_root_given;
 	bool		publish_via_partition_root;
 	AclResult	aclresult;
+	List	   *relations = NIL;
+	List	   *schemaidlist = NIL;
 
 	/* must have CREATE privilege on database */
 	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
@@ -224,13 +306,15 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	if (stmt->tables)
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &schemaidlist);
+	if (relations != NIL)
 	{
 		List	   *rels;
 
-		Assert(list_length(stmt->tables) > 0);
+		Assert(list_length(relations) > 0);
 
-		rels = OpenTableList(stmt->tables);
+		rels = OpenTableList(relations);
 		PublicationAddTables(puboid, rels, true, NULL);
 		CloseTableList(rels);
 	}
@@ -360,7 +444,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
  */
 static void
 AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
-					   HeapTuple tup)
+					   HeapTuple tup, List *tables, List *schemaidlist)
 {
 	List	   *rels = NIL;
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
@@ -374,13 +458,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
 
-	Assert(list_length(stmt->tables) > 0);
+	Assert(list_length(tables) > 0);
 
-	rels = OpenTableList(stmt->tables);
+	rels = OpenTableList(tables);
 
-	if (stmt->tableAction == DEFELEM_ADD)
+	if (stmt->action == DEFELEM_ADD)
 		PublicationAddTables(pubid, rels, false, stmt);
-	else if (stmt->tableAction == DEFELEM_DROP)
+	else if (stmt->action == DEFELEM_DROP)
 		PublicationDropTables(pubid, rels, false);
 	else						/* DEFELEM_SET */
 	{
@@ -398,10 +482,9 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
 
 			foreach(newlc, rels)
 			{
-				PublicationRelInfo *newpubrel;
+				Relation	newrel = (Relation) lfirst(newlc);
 
-				newpubrel = (PublicationRelInfo *) lfirst(newlc);
-				if (RelationGetRelid(newpubrel->relation) == oldrelid)
+				if (RelationGetRelid(newrel) == oldrelid)
 				{
 					found = true;
 					break;
@@ -410,16 +493,10 @@ AlterPublicationTables(AlterPublicationStmt *stmt, Relation rel,
 			/* Not yet in the list, open it and add to the list */
 			if (!found)
 			{
-				Relation	oldrel;
-				PublicationRelInfo *pubrel;
-
-				/* Wrap relation into PublicationRelInfo */
-				oldrel = table_open(oldrelid, ShareUpdateExclusiveLock);
+				Relation	oldrel = table_open(oldrelid,
+												ShareUpdateExclusiveLock);
 
-				pubrel = palloc(sizeof(PublicationRelInfo));
-				pubrel->relation = oldrel;
-
-				delrels = lappend(delrels, pubrel);
+				delrels = lappend(delrels, oldrel);
 			}
 		}
 
@@ -450,6 +527,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 	Relation	rel;
 	HeapTuple	tup;
 	Form_pg_publication pubform;
+	List	   *relations = NIL;
+	List	   *schemaidlist = NIL;
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
@@ -469,10 +548,16 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &schemaidlist);
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else
-		AlterPublicationTables(stmt, rel, tup);
+	{
+		if (relations)
+			AlterPublicationTables(stmt, rel, tup, relations, schemaidlist);
+	}
 
 	/* Cleanup. */
 	heap_freetuple(tup);
@@ -540,7 +625,7 @@ RemovePublicationById(Oid pubid)
 
 /*
  * Open relations specified by a PublicationTable list.
- * In the returned list of PublicationRelInfo, tables are locked
+ * In the returned list of Relation, tables are locked
  * in ShareUpdateExclusiveLock mode in order to add them to a publication.
  */
 static List *
@@ -555,16 +640,15 @@ OpenTableList(List *tables)
 	 */
 	foreach(lc, tables)
 	{
-		PublicationTable *t = lfirst_node(PublicationTable, lc);
-		bool		recurse = t->relation->inh;
+		RangeVar   *rv = lfirst_node(RangeVar, lc);
+		bool		recurse = rv->inh;
 		Relation	rel;
 		Oid			myrelid;
-		PublicationRelInfo *pub_rel;
 
 		/* Allow query cancel in case this takes a long time */
 		CHECK_FOR_INTERRUPTS();
 
-		rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+		rel = table_openrv(rv, ShareUpdateExclusiveLock);
 		myrelid = RelationGetRelid(rel);
 
 		/*
@@ -580,9 +664,7 @@ OpenTableList(List *tables)
 			continue;
 		}
 
-		pub_rel = palloc(sizeof(PublicationRelInfo));
-		pub_rel->relation = rel;
-		rels = lappend(rels, pub_rel);
+		rels = lappend(rels, rel);
 		relids = lappend_oid(relids, myrelid);
 
 		/*
@@ -615,9 +697,7 @@ OpenTableList(List *tables)
 
 				/* find_all_inheritors already got lock */
 				rel = table_open(childrelid, NoLock);
-				pub_rel = palloc(sizeof(PublicationRelInfo));
-				pub_rel->relation = rel;
-				rels = lappend(rels, pub_rel);
+				rels = lappend(rels, rel);
 				relids = lappend_oid(relids, childrelid);
 			}
 		}
@@ -638,10 +718,9 @@ CloseTableList(List *rels)
 
 	foreach(lc, rels)
 	{
-		PublicationRelInfo *pub_rel;
+		Relation	rel = (Relation) lfirst(lc);
 
-		pub_rel = (PublicationRelInfo *) lfirst(lc);
-		table_close(pub_rel->relation, NoLock);
+		table_close(rel, NoLock);
 	}
 }
 
@@ -658,8 +737,7 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 
 	foreach(lc, rels)
 	{
-		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
-		Relation	rel = pub_rel->relation;
+		Relation	rel = (Relation) lfirst(lc);
 		ObjectAddress obj;
 
 		/* Must be owner of the table or superuser. */
@@ -667,7 +745,7 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
 						   RelationGetRelationName(rel));
 
-		obj = publication_add_relation(pubid, pub_rel, if_not_exists);
+		obj = publication_add_relation(pubid, rel, if_not_exists);
 		if (stmt)
 		{
 			EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress,
@@ -691,8 +769,7 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 
 	foreach(lc, rels)
 	{
-		PublicationRelInfo *pubrel = (PublicationRelInfo *) lfirst(lc);
-		Relation	rel = pubrel->relation;
+		Relation	rel = (Relation) lfirst(lc);
 		Oid			relid = RelationGetRelid(rel);
 
 		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 228387eaee..ade93023b8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4817,7 +4817,7 @@ _copyCreatePublicationStmt(const CreatePublicationStmt *from)
 
 	COPY_STRING_FIELD(pubname);
 	COPY_NODE_FIELD(options);
-	COPY_NODE_FIELD(tables);
+	COPY_NODE_FIELD(pubobjects);
 	COPY_SCALAR_FIELD(for_all_tables);
 
 	return newnode;
@@ -4830,9 +4830,9 @@ _copyAlterPublicationStmt(const AlterPublicationStmt *from)
 
 	COPY_STRING_FIELD(pubname);
 	COPY_NODE_FIELD(options);
-	COPY_NODE_FIELD(tables);
+	COPY_NODE_FIELD(pubobjects);
 	COPY_SCALAR_FIELD(for_all_tables);
-	COPY_SCALAR_FIELD(tableAction);
+	COPY_SCALAR_FIELD(action);
 
 	return newnode;
 }
@@ -4958,12 +4958,14 @@ _copyForeignKeyCacheInfo(const ForeignKeyCacheInfo *from)
 	return newnode;
 }
 
-static PublicationTable *
-_copyPublicationTable(const PublicationTable *from)
+static PublicationObjSpec *
+_copyPublicationObject(const PublicationObjSpec *from)
 {
-	PublicationTable *newnode = makeNode(PublicationTable);
+	PublicationObjSpec *newnode = makeNode(PublicationObjSpec);
 
-	COPY_NODE_FIELD(relation);
+	COPY_SCALAR_FIELD(pubobjtype);
+	COPY_NODE_FIELD(object);
+	COPY_LOCATION_FIELD(location);
 
 	return newnode;
 }
@@ -5887,8 +5889,8 @@ copyObjectImpl(const void *from)
 		case T_PartitionCmd:
 			retval = _copyPartitionCmd(from);
 			break;
-		case T_PublicationTable:
-			retval = _copyPublicationTable(from);
+		case T_PublicationObjSpec:
+			retval = _copyPublicationObject(from);
 			break;
 
 			/*
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 800f588b5c..d384af2db7 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2302,7 +2302,7 @@ _equalCreatePublicationStmt(const CreatePublicationStmt *a,
 {
 	COMPARE_STRING_FIELD(pubname);
 	COMPARE_NODE_FIELD(options);
-	COMPARE_NODE_FIELD(tables);
+	COMPARE_NODE_FIELD(pubobjects);
 	COMPARE_SCALAR_FIELD(for_all_tables);
 
 	return true;
@@ -2314,9 +2314,9 @@ _equalAlterPublicationStmt(const AlterPublicationStmt *a,
 {
 	COMPARE_STRING_FIELD(pubname);
 	COMPARE_NODE_FIELD(options);
-	COMPARE_NODE_FIELD(tables);
+	COMPARE_NODE_FIELD(pubobjects);
 	COMPARE_SCALAR_FIELD(for_all_tables);
-	COMPARE_SCALAR_FIELD(tableAction);
+	COMPARE_SCALAR_FIELD(action);
 
 	return true;
 }
@@ -3134,12 +3134,12 @@ _equalBitString(const BitString *a, const BitString *b)
 }
 
 static bool
-_equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
+_equalPublicationObject(const PublicationObjSpec *a, const PublicationObjSpec *b)
 {
-	COMPARE_NODE_FIELD(relation);
+	COMPARE_NODE_FIELD(object);
 
 	return true;
-}
+} 
 
 /*
  * equal
@@ -3894,8 +3894,8 @@ equal(const void *a, const void *b)
 		case T_PartitionCmd:
 			retval = _equalPartitionCmd(a, b);
 			break;
-		case T_PublicationTable:
-			retval = _equalPublicationTable(a, b);
+		case T_PublicationObjSpec:
+			retval = _equalPublicationObject(a, b);
 			break;
 
 		default:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e3068a374e..f7d7cab596 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -195,6 +195,9 @@ static Node *makeXmlExpr(XmlExprOp op, char *name, List *named_args,
 static List *mergeTableFuncParameters(List *func_args, List *columns);
 static TypeName *TableFuncTypeName(List *columns);
 static RangeVar *makeRangeVarFromAnyName(List *names, int position, core_yyscan_t yyscanner);
+static RangeVar *makeRangeVarFromQualifiedName(char *name, List *rels,
+											   int location,
+											   core_yyscan_t yyscanner);
 static void SplitColQualList(List *qualList,
 							 List **constraintList, CollateClause **collClause,
 							 core_yyscan_t yyscanner);
@@ -256,6 +259,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	PartitionSpec		*partspec;
 	PartitionBoundSpec	*partboundspec;
 	RoleSpec			*rolespec;
+	PublicationObjSpec	*publicationobjectspec;
 	struct SelectLimit	*selectlimit;
 	SetQuantifier	 setquantifier;
 	struct GroupClause  *groupclause;
@@ -425,14 +429,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				transform_element_list transform_type_list
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
-				drop_option_list publication_table_list
+				drop_option_list pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
 %type <list>	group_by_list
 %type <node>	group_by_item empty_grouping_set rollup_clause cube_clause
 %type <node>	grouping_sets_clause
-%type <node>	opt_publication_for_tables publication_for_tables publication_table
 
 %type <list>	opt_fdw_options fdw_options
 %type <defelt>	fdw_option
@@ -517,6 +520,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	table_ref
 %type <jexpr>	joined_table
 %type <range>	relation_expr
+%type <range>	extended_relation_expr
 %type <range>	relation_expr_opt_alias
 %type <node>	tablesample_clause opt_repeatable_clause
 %type <target>	target_el set_target insert_column_item
@@ -554,6 +558,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	var_value zone_value
 %type <rolespec> auth_ident RoleSpec opt_granted_by
 
+%type <publicationobjectspec> PublicationObjSpec
+%type <publicationobjectspec> pubobj_expr
+%type <node> 	pubobj_name
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
@@ -9591,69 +9598,117 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 
 /*****************************************************************************
  *
- * CREATE PUBLICATION name [ FOR TABLE ] [ WITH options ]
+ * CREATE PUBLICATION name [WITH options]
+ *
+ * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ *
+ * CREATE PUBLICATION FOR pub_obj [, pub_obj2] [WITH options]
+ *
+ * pub_obj is one of:
+ *
+ *		TABLE table [, table2]
+ *		ALL TABLES IN SCHEMA schema [, schema2]
  *
  *****************************************************************************/
 
 CreatePublicationStmt:
-			CREATE PUBLICATION name opt_publication_for_tables opt_definition
+			CREATE PUBLICATION name opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $5;
-					if ($4 != NULL)
-					{
-						/* FOR TABLE */
-						if (IsA($4, List))
-							n->tables = (List *)$4;
-						/* FOR ALL TABLES */
-						else
-							n->for_all_tables = true;
-					}
+					n->options = $4;
+					$$ = (Node *)n;
+				}
+			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+				{
+					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
+					n->pubname = $3;
+					n->options = $7;
+					n->for_all_tables = true;
+					$$ = (Node *)n;
+				}
+			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
+				{
+					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
+					n->pubname = $3;
+					n->options = $6;
+					n->pubobjects = (List *)$5;
 					$$ = (Node *)n;
 				}
 		;
 
-opt_publication_for_tables:
-			publication_for_tables					{ $$ = $1; }
-			| /* EMPTY */							{ $$ = NULL; }
+pubobj_expr:
+			pubobj_name
+				{
+					/* inheritance query, implicitly */
+					$$ = makeNode(PublicationObjSpec);
+					$$->object = $1;
+				}
+			| extended_relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->object = $1;
+				}
+			| CURRENT_SCHEMA
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->object = makeString("CURRENT_SCHEMA");
+				}
 		;
 
-publication_for_tables:
-			FOR TABLE publication_table_list
+/* This can be either a schema or relation name. */
+pubobj_name:
+			ColId
 				{
-					$$ = (Node *) $3;
+					$$ = (Node *) makeString($1);
 				}
-			| FOR ALL TABLES
+			| ColId indirection
 				{
-					$$ = (Node *) makeInteger(true);
+					$$ = (Node *) makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
 				}
 		;
 
-publication_table_list:
-			publication_table
-					{ $$ = list_make1($1); }
-		| publication_table_list ',' publication_table
-				{ $$ = lappend($1, $3); }
+/* FOR TABLE and FOR ALL TABLES IN SCHEMA specifications */
+PublicationObjSpec:	TABLE pubobj_expr
+					{
+						$$ = $2;
+						$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+						$$->location = @1;
+					}
+			| ALL TABLES IN_P SCHEMA pubobj_expr
+					{
+						$$ = $5;
+						$$->pubobjtype = PUBLICATIONOBJ_REL_IN_SCHEMA;
+						$$->location = @1;
+					}
+			| pubobj_expr
+					{
+						$$ = $1;
+						$$->pubobjtype = PUBLICATIONOBJ_UNKNOWN;
+						$$->location = @1;
+					}
 		;
 
-publication_table: relation_expr
-		{
-			PublicationTable *n = makeNode(PublicationTable);
-			n->relation = $1;
-			$$ = (Node *) n;
-		}
+pub_obj_list: 	PublicationObjSpec
+					{ $$ = list_make1($1); }
+			| pub_obj_list ',' PublicationObjSpec
+					{ $$ = lappend($1, $3); }
 	;
 
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
  *
- * ALTER PUBLICATION name ADD TABLE table [, table2]
+ * ALTER PUBLICATION name ADD pub_obj [, pub_obj ...]
  *
- * ALTER PUBLICATION name DROP TABLE table [, table2]
+ * ALTER PUBLICATION name DROP pub_obj [, pub_obj ...]
  *
- * ALTER PUBLICATION name SET TABLE table [, table2]
+ * ALTER PUBLICATION name SET pub_obj [, pub_obj ...]
+ *
+ * pub_obj is one of:
+ *
+ *		TABLE table_name [, table_name ...]
+ *		ALL TABLES IN SCHEMA schema_name [, schema_name ...]
  *
  *****************************************************************************/
 
@@ -9665,28 +9720,28 @@ AlterPublicationStmt:
 					n->options = $5;
 					$$ = (Node *)n;
 				}
-			| ALTER PUBLICATION name ADD_P TABLE publication_table_list
+			| ALTER PUBLICATION name ADD_P pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
-					n->tables = $6;
-					n->tableAction = DEFELEM_ADD;
+					n->pubobjects = $5;
+					n->action = DEFELEM_ADD;
 					$$ = (Node *)n;
 				}
-			| ALTER PUBLICATION name SET TABLE publication_table_list
+			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
-					n->tables = $6;
-					n->tableAction = DEFELEM_SET;
+					n->pubobjects = $5;
+					n->action = DEFELEM_SET;
 					$$ = (Node *)n;
 				}
-			| ALTER PUBLICATION name DROP TABLE publication_table_list
+			| ALTER PUBLICATION name DROP pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
-					n->tables = $6;
-					n->tableAction = DEFELEM_DROP;
+					n->pubobjects = $5;
+					n->action = DEFELEM_DROP;
 					$$ = (Node *)n;
 				}
 		;
@@ -12430,7 +12485,14 @@ relation_expr:
 					$$->inh = true;
 					$$->alias = NULL;
 				}
-			| qualified_name '*'
+			| extended_relation_expr
+				{
+					$$ = $1;
+				}
+		;
+
+extended_relation_expr:
+			qualified_name '*'
 				{
 					/* inheritance query, explicitly */
 					$$ = $1;
@@ -12453,7 +12515,6 @@ relation_expr:
 				}
 		;
 
-
 relation_expr_list:
 			relation_expr							{ $$ = list_make1($1); }
 			| relation_expr_list ',' relation_expr	{ $$ = lappend($1, $3); }
@@ -15104,28 +15165,7 @@ qualified_name:
 				}
 			| ColId indirection
 				{
-					check_qualified_name($2, yyscanner);
-					$$ = makeRangeVar(NULL, NULL, @1);
-					switch (list_length($2))
-					{
-						case 1:
-							$$->catalogname = NULL;
-							$$->schemaname = $1;
-							$$->relname = strVal(linitial($2));
-							break;
-						case 2:
-							$$->catalogname = $1;
-							$$->schemaname = strVal(linitial($2));
-							$$->relname = strVal(lsecond($2));
-							break;
-						default:
-							ereport(ERROR,
-									(errcode(ERRCODE_SYNTAX_ERROR),
-									 errmsg("improper qualified name (too many dotted names): %s",
-											NameListToString(lcons(makeString($1), $2))),
-									 parser_errposition(@1)));
-							break;
-					}
+					$$ = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
 				}
 		;
 
@@ -17045,6 +17085,41 @@ TableFuncTypeName(List *columns)
 	return result;
 }
 
+/*
+ * Convert a relation_name with name and namelist to a RangeVar using
+ * makeRangeVar.
+ */
+static RangeVar *
+makeRangeVarFromQualifiedName(char *name, List *namelist, int location,
+							  core_yyscan_t yyscanner)
+{
+	RangeVar *r = makeRangeVar(NULL, NULL, location);
+
+	check_qualified_name(namelist, yyscanner);
+	switch (list_length(namelist))
+	{
+		case 1:
+			r->catalogname = NULL;
+			r->schemaname = name;
+			r->relname = strVal(linitial(namelist));
+			break;
+		case 2:
+			r->catalogname = name;
+			r->schemaname = strVal(linitial(namelist));
+			r->relname = strVal(lsecond(namelist));
+			break;
+		default:
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("improper qualified name (too many dotted names): %s",
+						   NameListToString(lcons(makeString(name), namelist))),
+						   parser_errposition(location));
+			break;
+	}
+
+	return r;
+}
+
 /*
  * Convert a list of (dotted) names to a RangeVar (like
  * makeRangeVarFromNameList, but with position support).  The
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 561266aa3e..f332bad4d4 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -83,11 +83,6 @@ typedef struct Publication
 	PublicationActions pubactions;
 } Publication;
 
-typedef struct PublicationRelInfo
-{
-	Relation	relation;
-} PublicationRelInfo;
-
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
@@ -113,7 +108,7 @@ extern List *GetAllTablesPublications(void);
 extern List *GetAllTablesPublicationRelations(bool pubviaroot);
 
 extern bool is_publishable_relation(Relation rel);
-extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
+extern ObjectAddress publication_add_relation(Oid pubid, Relation targetrel,
 											  bool if_not_exists);
 
 extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index b3ee4194d3..40aadb37d9 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -487,7 +487,7 @@ typedef enum NodeTag
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
 	T_VacuumRelation,
-	T_PublicationTable,
+	T_PublicationObjSpec,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 45e4f2a16e..6f1239017e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -353,6 +353,26 @@ typedef struct RoleSpec
 	int			location;		/* token location, or -1 if unknown */
 } RoleSpec;
 
+/*
+ * Publication object type
+ */
+typedef enum PublicationObjSpecType
+{
+	PUBLICATIONOBJ_TABLE,				/* Table type */
+	PUBLICATIONOBJ_REL_IN_SCHEMA,		/* Relations in schema type */
+	PUBLICATIONOBJ_UNKNOWN				/* Unknown type */
+} PublicationObjSpecType;
+
+typedef struct PublicationObjSpec
+{
+	NodeTag		type;
+	PublicationObjSpecType pubobjtype;	/* type of this publication object */
+	void	   *object;			/* publication object could be:
+								 * RangeVar - table object
+								 * String	- tablename or schemaname */
+	int			location;		/* token location, or -1 if unknown */
+} PublicationObjSpec;
+
 /*
  * FuncCall - a function or aggregate invocation
  *
@@ -3636,18 +3656,12 @@ typedef struct AlterTSConfigurationStmt
 	bool		missing_ok;		/* for DROP - skip error if missing? */
 } AlterTSConfigurationStmt;
 
-typedef struct PublicationTable
-{
-	NodeTag		type;
-	RangeVar   *relation;		/* relation to be published */
-} PublicationTable;
-
 typedef struct CreatePublicationStmt
 {
 	NodeTag		type;
 	char	   *pubname;		/* Name of the publication */
 	List	   *options;		/* List of DefElem nodes */
-	List	   *tables;			/* Optional list of tables to add */
+	List	   *pubobjects;		/* Optional list of publication objects */
 	bool		for_all_tables; /* Special publication for all tables in db */
 } CreatePublicationStmt;
 
@@ -3659,10 +3673,11 @@ typedef struct AlterPublicationStmt
 	/* parameters used for ALTER PUBLICATION ... WITH */
 	List	   *options;		/* List of DefElem nodes */
 
-	/* parameters used for ALTER PUBLICATION ... ADD/DROP TABLE */
-	List	   *tables;			/* List of tables to add/drop */
+	/* ALTER PUBLICATION ... ADD/DROP TABLE/ALL TABLES IN SCHEMA parameters */
+	List	   *pubobjects;		/* Optional list of publication objects */
 	bool		for_all_tables; /* Special publication for all tables in db */
-	DefElemAction tableAction;	/* What action to perform with the tables */
+	DefElemAction action;		/* What action to perform with the
+								 * tables/schemas */
 } AlterPublicationStmt;
 
 typedef struct CreateSubscriptionStmt
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 423780652f..8a1b97836e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2045,8 +2045,9 @@ PsqlSettings
 Publication
 PublicationActions
 PublicationInfo
+PublicationObjSpec
+PublicationObjSpecType
 PublicationPartOpt
-PublicationRelInfo
 PublicationTable
 PullFilter
 PullFilterOps

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 15 Sep 2021 09:36:51 -0300

> SEQUENCE seq1,seq2, ALL SEQUENCES IN SCHEMA s3,s4;

Oh, thanks, it looks like this can be useful.  We can get the common
grammar done and then rebase all the other patches (I was also just told
about support for sequences in [1]) on top.

[1] https://postgr.es/m/[email-masked]

> Columns can be added to PublicationObjSpec data structure.

Right.  (As a List of String, I imagine.)

> The patch
> will be then handled accordingly during post processing.

Yeah, I think it'd be cleaner if the node type has two members, something like
this

typedef struct PublicationObjSpec
{
	NodeTag		type;
	PublicationObjSpecType pubobjtype;	/* type of this publication object */
	RangeVar	*rv;		/* if a table */
	String		*objname;	/* if a schema */
	int		location;		/* token location, or -1 if unknown */
} PublicationObjSpec;

and only one of them is set, the other is NULL, depending on the object type.

---

From: "Euler Taveira" <[email-masked]>
Date: Wed, 15 Sep 2021 11:46:34 -0300

> SEQUENCE seq1,seq2, ALL SEQUENCES IN SCHEMA s3,s4;
I don't like this syntax. It seems too much syntax for the same purpose in a
single command. If you look at GRANT command whose ALL TABLES IN SCHEMA syntax
was extracted, you can use ON TABLE or ON ALL TABLES IN SCHEMA; you cannot use
both. This proposal allows duplicate objects (of course, you can ignore it but
the current code prevent duplicates -- see publication_add_relation).

IMO you should mimic the GRANT grammar and have multiple commands for row
filtering, column filtering, and ALL FOO IN SCHEMA. The filtering patches only
use the FOR TABLE syntax. The later won't have filtering syntax. Having said
that the grammar should be:

CREATE PUBLICATION name
    [ FOR TABLE [ ONLY ] table_name [ * ] [ (column_name [, ...] ) ] [ WHERE (expression) ] [, ...]
      | FOR ALL TABLES
      | FOR ALL TABLES IN SCHEMA schema_name, [, ...]
      | FOR ALL SEQUENCES IN SCHEMA schema_name, [, ...] ]
    [ WITH ( publication_parameter [= value] [, ... ] ) ]

ALTER PUBLICATION name ADD TABLE [ ONLY ] table_name [ * ] [ (column_name [, ...] ) ] [ WHERE (expression) ]
ALTER PUBLICATION name ADD ALL TABLES IN SCHEMA schema_name, [, ...]
ALTER PUBLICATION name ADD ALL SEQUENCES IN SCHEMA schema_name, [, ...]

ALTER PUBLICATION name SET TABLE [ ONLY ] table_name [ * ] [ (column_name [, ...] ) ] [ WHERE (expression) ]
ALTER PUBLICATION name SET ALL TABLES IN SCHEMA schema_name, [, ...]
ALTER PUBLICATION name SET ALL SEQUENCES IN SCHEMA schema_name, [, ...]

ALTER PUBLICATION name DROP TABLE [ ONLY ] table_name [ * ]
ALTER PUBLICATION name DROP ALL TABLES IN SCHEMA schema_name, [, ...]
ALTER PUBLICATION name DROP ALL SEQUENCES IN SCHEMA schema_name, [, ...]

Opinions?

---

From: "[email-masked]" <[email-masked]>
Date: Thu, 16 Sep 2021 02:06:09 +0000

On Wednesday, September 15, 2021 8:19 PM vignesh C <[email-masked]> wrote:
> I have extracted the parser code and attached it here, so that it will be easy to
> go through. We wanted to support the following syntax as in
> [1]:
> TABLE t1,t2,t3, ALL TABLES IN SCHEMA s1,s2, SEQUENCE seq1,seq2, ALL
> SEQUENCES IN SCHEMA s3,s4;

I am +1 for this syntax.

This syntax is more flexible than adding or dropping different type objects in
separate commands. User can either use one single command to add serval different
objects or use serval commands to add each type objects.

Best regards,
Hou zj

---

From: Peter Smith <[email-masked]>
Date: Thu, 16 Sep 2021 13:06:58 +1000

On Tue, Sep 7, 2021 at 3:51 AM Alvaro Herrera <[email-masked]> wrote:
...
> because that would perhaps cause you too much merge pain.

I noticed that the latest v5 no longer includes the TAP test which was
in the v4 patch.

(src/test/subscription/t/021_column_filter.pl)

Was that omission deliberate?

------
Kind Regards,
Peter Smith.
Fujitsu Australia.

---

From: Amit Kapila <[email-masked]>
Date: Thu, 16 Sep 2021 08:45:15 +0530

> and only one of them is set, the other is NULL, depending on the object type.

I think the problem here is that with the proposed grammar we won't be
always able to distinguish names at the gram.y stage. Some post
parsing analysis is required to attribute the right type to name as is
done in the patch. The same seems to be indicated by Tom in his email
as well where he has proposed this syntax [1]. Also, something similar
is done for privilege_target (GRANT syntax) where we have a list of
objects but here the story is slightly more advanced because we are
planning to allow specifying multiple objects in one command. One
might think that we can identify each type of objects lists separately
but that gives grammar conflicts as it is not able to identify whether
the comma ',' is used for the same type object or for the next type.
Due to which we need to come up with a generic object for names to
which we attribute the right type in post parse analysis. Now, I think
instead of void *, it might be better to use Node * for generic
objects unless we have some problem.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Thu, 16 Sep 2021 08:52:56 +0530

On Wed, Sep 15, 2021 at 8:19 PM Euler Taveira <[email-masked]> wrote:
> use the FOR TABLE syntax. The later won't have filtering syntax.
>

Sure, but we don't prevent if the user uses only FOR TABLE variant.
OTOH, it is better to provide flexibility to allow multiple objects in
one command unless that is not feasible. It saves the effort of users
in many cases. In short, +1 for the syntax where multiple objects can
be allowed.

-- 
With Regards,
Amit Kapila.

---

From: vignesh C <[email-masked]>
Date: Thu, 16 Sep 2021 10:35:21 +0530

On Thu, Sep 16, 2021 at 8:45 AM Amit Kapila <[email-masked]> wrote:
> always able to distinguish names at the gram.y stage.

This is the issue that Amit was talking about:
gram.y: error: shift/reduce conflicts: 2 found, 0 expected
gram.y: warning: shift/reduce conflict on token ',' [-Wcounterexamples]
  First example: CREATE PUBLICATION name FOR TABLE relation_expr_list
• ',' relation_expr ',' PublicationObjSpec opt_definition $end
  Shift derivation
    $accept
    ↳ parse_toplevel
                                                              $end
      ↳ stmtmulti
        ↳ toplevel_stmt
          ↳ stmt
            ↳ CreatePublicationStmt
              ↳ CREATE PUBLICATION name FOR pub_obj_list
                                               opt_definition
                                            ↳ PublicationObjSpec
                        ',' PublicationObjSpec
                                              ↳ TABLE relation_expr_list
                                                      ↳
relation_expr_list • ',' relation_expr
  Second example: CREATE PUBLICATION name FOR TABLE relation_expr_list
• ',' PublicationObjSpec opt_definition $end
  Reduce derivation
    $accept
    ↳ parse_toplevel
                                            $end
      ↳ stmtmulti
        ↳ toplevel_stmt
          ↳ stmt
            ↳ CreatePublicationStmt
              ↳ CREATE PUBLICATION name FOR pub_obj_list
                             opt_definition
                                            ↳ pub_obj_list
      ',' PublicationObjSpec
                                              ↳ PublicationObjSpec
                                                ↳ TABLE relation_expr_list •
Here it is not able to distinguish if ',' is used for the next table
name or the next object.
I was able to reproduce this issue with the attached patch.

Regards,
Vignesh

 	/* Form a tuple. */

 	return myself;
 }
 
+/*

-	if (stmt->tables)

 

 
 			foreach(newlc, rels)
 			{

 

 
 
 	/* Cleanup. */
 	heap_freetuple(tup);
 
 /*

 
 		/*

 		/*

 	foreach(lc, rels)
 	{

 
 	foreach(lc, rels)
 	{

 
 	foreach(lc, rels)
 	{

 
 	return newnode;

 	return newnode;
 }

 
 	return newnode;
 }
 
 			/*
 
 	return true;
 
 	return true;
 }
 
 static bool
 
 	return true;
-}
+} 
 
 /*
  * equal
 
 		default:
index e3068a374e..c50bb570ea 100644
+				drop_option_list pub_obj_list pubobj_expr_list

@@ -554,6 +554,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -9591,69 +9594,107 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec

 CreatePublicationStmt:

-publication_for_tables:
 
-publication_table_list:
-			publication_table
-					{ $$ = list_make1($1); }
-		| publication_table_list ',' publication_table
-				{ $$ = lappend($1, $3); }
+/* FOR TABLE and FOR ALL TABLES IN SCHEMA specifications */
+PublicationObjSpec:	TABLE relation_expr_list
+					{
+						$$ = $2;
+						$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+						$$->location = @1;
+					}
+			| ALL TABLES IN_P SCHEMA pubobj_expr_list
+					{
+						$$ = $5;
+						$$->pubobjtype = PUBLICATIONOBJ_REL_IN_SCHEMA;
+						$$->location = @1;
+					}
 		;
 
-publication_table: relation_expr
-		{
-			PublicationTable *n = makeNode(PublicationTable);
-			n->relation = $1;
-			$$ = (Node *) n;
-		}
+pubobj_expr_list: 	pubobj_expr
+					{ $$ = list_make1($1); }
+			| pubobj_expr_list ',' pubobj_expr
+					{ $$ = lappend($1, $3); }
+	;
+
+pub_obj_list: 	PublicationObjSpec
+					{ $$ = list_make1($1); }
+			| pub_obj_list ',' PublicationObjSpec
+					{ $$ = lappend($1, $3); }
 	;

@@ -9665,28 +9706,28 @@ AlterPublicationStmt:
 					n->options = $5;
 					$$ = (Node *)n;
 				}

 
index e0057daa06..d34b4ac8e5 100644
 
 	/*
index 3138877553..20eeb12022 100644
 
+/*

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 16 Sep 2021 09:44:35 -0300

On 2021-Sep-16, Amit Kapila wrote:

> done in the patch.

Doesn't it work to stuff them all into RangeVars?  Then you don't need
to make the node type a monstrosity, just bail out in parse analysis if
an object spec has more elements in the RV than the object type allows.

---

From: Amit Kapila <[email-masked]>
Date: Thu, 16 Sep 2021 19:18:29 +0530

> an object spec has more elements in the RV than the object type allows.

So, are you suggesting that we store even schema names corresponding
to FOR ALL TABLES IN SCHEMA s1 [, ...] grammar in RangeVars in some
way (say store schema name in relname or schemaname field of RangeVar)
at gram.y stage and then later extract it from RangeVar? If so, why do
you think it would be better than the current proposed way?

-- 
With Regards,
Amit Kapila.

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 16 Sep 2021 10:50:25 -0300

> +++ b/src/backend/parser/gram.y

Yeah, on a quick glance this looks all wrong.  Your PublicationObjSpec
production should return a node with tag PublicationObjSpec, and
pubobj_expr should not exist at all -- that stuff is just making it all
more confusing.

I think it'd be something like this:

PublicationObjSpec:	
			ALL TABLES
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_ALL_TABLES;
						$$->location = @1;
					}
			| TABLE qualified_name
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_TABLE;
						$$->pubobj = $2;
						$$->location = @1;
					}
			| ALL TABLES IN_P SCHEMA name
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_ALL_TABLES_IN_SCHEMA;
						$$->pubobj = makeRangeVar( ... $5 ... );
						$$->location = @1;
					}
			| qualified_name
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
						$$->pubobj = $1;
						$$->location = @1;
					};

You need a single object name under TABLE, not a list -- this was Tom's
point about needing post-processing to determine how to assign a type to
a object that's what I named PUBLICATIONOBJ_CONTINUATION here.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Puedes vivir sólo una vez, pero si lo haces bien, una vez es suficiente"

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 16 Sep 2021 11:36:32 -0300

On 2021-Sep-16, Alvaro Herrera wrote:

> 						$$->pubrvobj = $2;
> 						$$->pubplainobj = $5;

So you don't have to cram the schema name in a RangeVar, which would
indeed be quite awkward.  (I'm sure you can come up with better names
for the struct members there ...)

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Porque francamente, si para saber manejarse a uno mismo hubiera que
rendir examen... ¿Quién es el machito que tendría carnet?"  (Mafalda)

---

From: vignesh C <[email-masked]>
Date: Fri, 17 Sep 2021 09:36:41 +0530

> a object that's what I named PUBLICATIONOBJ_CONTINUATION here.

In the above, we will not be able to use qualified_name, as
qualified_name will not support the following syntaxes:
create publication pub1 for table t1 *;
create publication pub1 for table ONLY t1 *;
create publication pub1 for table ONLY (t1);

To solve this problem we can change qualified_name to relation_expr
but the problem with doing that is that the user will be able to
provide the following syntaxes:
create publication pub1 for all tables in schema sch1 *;
create publication pub1 for all tables in schema ONLY sch1 *;
create publication pub1 for all tables in schema ONLY (sch1);

To handle this we will need some special flag which will differentiate
these and throw errors at post processing time. We need to define an
expression similar to relation_expr say pub_expr which handles all
variants of qualified_name and then use a special flag so that we can
throw an error if somebody uses the above type of syntax for schema
names. And then if we have to distinguish between schema name and
relation name variant, then we need few other things.

We proposed the below solution which handles all these problems and
also used Node type which need not store schemaname in RangeVar type:
pubobj_expr:
                        pubobj_name
                                {
                                        /* inheritance query, implicitly */
                                        $$ = makeNode(PublicationObjSpec);
                                        $$->object = $1;
                                }
                        | extended_relation_expr
                                {
                                        $$ = makeNode(PublicationObjSpec);
                                        $$->object = (Node *)$1;
                                }
                        | CURRENT_SCHEMA
                                {
                                        $$ = makeNode(PublicationObjSpec);
                                        $$->object = (Node
*)makeString("CURRENT_SCHEMA");
                                }
                ;
/* This can be either a schema or relation name. */
pubobj_name:
                        ColId
                                {
                                        $$ = (Node *) makeString($1);
                                }
                        | ColId indirection
                                {
                                        $$ = (Node *)
makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
                                }
                ;
/* FOR TABLE and FOR ALL TABLES IN SCHEMA specifications */
PublicationObjSpec:     TABLE pubobj_expr
                                        {
                                                $$ = $2;
                                                $$->pubobjtype =
PUBLICATIONOBJ_TABLE;
                                                $$->location = @1;
                                        }
                        | ALL TABLES IN_P SCHEMA pubobj_expr
                                        {
                                                $$ = $5;
                                                $$->pubobjtype =
PUBLICATIONOBJ_REL_IN_SCHEMA;
                                                $$->location = @1;
                                        }
                        | pubobj_expr
                                        {
                                                $$ = $1;
                                                $$->pubobjtype =
PUBLICATIONOBJ_UNKNOWN;
                                                $$->location = @1;
                                        }
                ;

The same has been proposed in the recent version of patch [1].
[1] - https://www.postgresql.org/message-id/CALDaNm0OudeDeFN7bSWPro0hgKx%3D1zPgcNFWnvU_G6w3mDPX0Q%40mail.gmail.com
Thoughts?

Regards,
Vignesh

---

From: "[email-masked]" <[email-masked]>
Date: Fri, 17 Sep 2021 05:08:36 +0000

On Thurs, Sep 16, 2021 10:37 PM Alvaro Herrera <[email-masked]> wrote:
> So you don't have to cram the schema name in a RangeVar, which would indeed
> be quite awkward.  (I'm sure you can come up with better names for the struct
> members there ...)> 

Did you mean something like the following ?
-----
PublicationObjSpec:
		TABLE qualified_name {...}
		| ALL TABLES IN_P SCHEMA name {...}
		;

pub_obj_list:
		PublicationObjSpec
        | pub_obj_list ',' PublicationObjSpec
-----

If so, I think it only supports syntaxes like "TABLE a, TABLE b, TABLE c" while
we cannnot use "TABLE a,b,c". To support multiple objects, we need a bare name
in PublicationObjSpec.

Or Did you mean something like this ?
-----
PublicationObjSpec:
		TABLE qualified_name {...}
		| ALL TABLES IN_P SCHEMA name {...}
		| qualified_name {...}
		;
-----

I think this doesn't support relation expression like "table */ONLY table/ONLY
(table)" as memtioned by Vignesh [1].

Thoughts ?

[1] https://www.postgresql.org/message-id/CALDaNm06%3DLDytYyY%2BxcAQd8UK_YpJ3zMo4P5V8KBArw6MoDWDg%40mail.gmail.com

Best regards,
Hou zj

---

From: Amit Kapila <[email-masked]>
Date: Thu, 23 Sep 2021 15:48:57 +0530

> also used Node type which need not store schemaname in RangeVar type:

Alvaro, do you have any thoughts on these proposed grammar changes?

-- 
With Regards,
Amit Kapila.

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 23 Sep 2021 21:15:12 +0200

Hi,

I wanted to do a review of this patch, but I'm a bit confused about 
which patch(es) to review. There's the v5 patch, and then these two 
patches - which seem to be somewhat duplicate, though.

Can anyone explain what's the "current" patch version, or perhaps tell 
me which of the patches to combine?

regards

---

From: Amit Kapila <[email-masked]>
Date: Fri, 24 Sep 2021 08:40:30 +0530

> me which of the patches to combine?

I think v5 won't work atop a common grammar patch. There need some
adjustments in v5. I think it would be good if we can first get the
common grammar patch reviewed/committed and then build this on top of
it. The common grammar and the corresponding implementation are being
accomplished in the Schema support patch, the latest version of which
is at [1]. Now, Vignesh seems to have extracted just the grammar
portion of that work in his patch
Generic_object_type_parser_002_table_schema_publication [2] (there are
some changes after that but not anything fundamentally different till
now) then he seems to have prepared a patch
(Generic_object_type_parser_001_table_publication [2]) on similar
lines only for tables.

[1] - https://www.postgresql.org/message-id/OS3PR01MB571844A87B6A83B7C10F9D6B94A39%40OS3PR01MB5718.jpnprd01.prod.outlook.com
[2] - https://www.postgresql.org/message-id/CALDaNm1YoxJCs%3DuiyPM%3DtFDDc2qn0ja01nb2TCPqrjZH2jR0sQ%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: vignesh C <[email-masked]>
Date: Fri, 24 Sep 2021 10:35:29 +0530

On Fri, Sep 24, 2021 at 8:40 AM Amit Kapila <[email-masked]> wrote:
> is at [1].

I have posted an updated patch with the fixes at [1], please review
the updated patch.
[1] - https://www.postgresql.org/message-id/CALDaNm1R-xbQvz4LU5OXu3KKwbWOz3uDcT_YjRU6V0R5FZDYDg%40mail.gmail.com

Regards,
Vignesh

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 24 Sep 2021 10:25:09 -0300

> Alvaro, do you have any thoughts on these proposed grammar changes?

Yeah, I think pubobj_name remains a problem in that you don't know its
return type -- could be a String or a RangeVar, and the user of that
production can't distinguish.  So you're still (unnecessarily, IMV)
stashing an object of undetermined type into ->object.

I think you should get rid of both pubobj_name and pubobj_expr and do
somethine like this:

/* FOR TABLE and FOR ALL TABLES IN SCHEMA specifications */
PublicationObjSpec:	TABLE ColId
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_TABLE;
						$$->rangevar = makeRangeVarFromQualifiedName($1, NULL, @1, yyscanner);
					}
			| TABLE ColId indirection
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_TABLE;
						$$->rangevar = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
					}
			| ALL TABLES IN_P SCHEMA ColId
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_REL_IN_SCHEMA;
						$$->name = $4;
					}
			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA	/* XXX should this be "IN_P CURRENT_SCHEMA"? */
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_CURRSCHEMA;
						$$->name = $4;
					}
			| ColId
					{
						$$ = makeNode(PublicationObjSpec);
						$$->name = $1;
						$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
					}
			| ColId indirection
					{
						$$ = makeNode(PublicationObjSpec);
						$$->rangevar = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
						$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
					}
			| CURRENT_SCHEMA
					{
						$$ = makeNode(PublicationObjSpec);
						$$->pubobjtype = PUBLICATIONOBJ_CURRSCHEMA;
					}
		;

so in AlterPublicationStmt you would have stanzas like

			| ALTER PUBLICATION name ADD_P pub_obj_list
				{
					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
					n->pubname = $3;
					n->pubobjects = preprocess_pubobj_list($5);
					n->action = DEFELEM_ADD;
					$$ = (Node *)n;
				}

where preprocess_pubobj_list (defined right after processCASbits and
somewhat mimicking it and SplitColQualList) takes all
PUBLICATIONOBJ_CONTINUATION and turns them into either
PUBLICATIONOBJ_TABLE entries or PUBLICATIONOBJ_REL_IN_SCHEMA entries,
depending on what the previous entry was.  (And of course if there is no
previous entry, raise an error immediately).  Note that node
PublicationObjSpec now has two fields, one for RangeVar and another for
a plain name, and tables always use the second one, except when they are
continuations, but of course those continuations that use name are
turned into rangevars in the preprocess step.  I think that would make
the code in ObjectsInPublicationToOids less messy.

(I don't think using the string "CURRENT_SCHEMA" is a great solution.
Did you try having a schema named CURRENT_SCHEMA?)

I verified that bison is happy with the grammar I proposed; I also
verified that you can add opt_column_list to the stanzas for tables, and
it remains happy.

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
Y una voz del caos me habló y me dijo
"Sonríe y sé feliz, podría ser peor".
Y sonreí. Y fui feliz.
Y fue peor.

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 24 Sep 2021 22:54:40 +0200

> [1] - https://www.postgresql.org/message-id/CALDaNm1R-xbQvz4LU5OXu3KKwbWOz3uDcT_YjRU6V0R5FZDYDg%40mail.gmail.com

But that's not the column filtering patch, right? Why would this patch 
depend on "schema level support", but maybe the consensus is there's 
some common part that we need to get in first?

regards

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 24 Sep 2021 19:24:19 -0300

On 2021-Sep-24, Tomas Vondra wrote:

> depend on "schema level support", but maybe the consensus is there's some
> common part that we need to get in first?

Yes, the grammar needs to be common.  I posted a proposed grammar in
https://www.postgresql.org/message-id/202109241325.eag5g6mpvoup%40alvherre.pgsql
(this thread) which should serve both.  I forgot to test the addition of
a WHERE clause for row filtering, though, and I didn't think to look at
adding SEQUENCE support either.

(I'm not sure what's going to be the proposal regarding FOR ALL TABLES
IN SCHEMA for sequences.  Are we going to have "FOR ALL SEQUENCES IN
SCHEMA" and "FOR ALL TABLES AND SEQUENCES IN SCHEMA"?)

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
Thou shalt study thy libraries and strive not to reinvent them without
cause, that thy code may be short and readable and thy days pleasant
and productive. (7th Commandment for C Programmers)

---

From: Tomas Vondra <[email-masked]>
Date: Sat, 25 Sep 2021 00:30:12 +0200

> adding SEQUENCE support either.

Fine with me, but I still don't know which version of the column 
filtering patch should I look at ... maybe there's none up to date, at 
the moment?

> SCHEMA" and "FOR ALL TABLES AND SEQUENCES IN SCHEMA"?)

Should be "FOR ABSOLUTELY EVERYTHING IN SCHEMA" of course ;-)

On a more serious note, a comma-separated list of objects seems like the 
best / most flexible choice, i.e. "FOR TABLES, SEQUENCES IN SCHEMA"?

regards

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 24 Sep 2021 20:00:47 -0300

On 2021-Sep-25, Tomas Vondra wrote:

> Fine with me, but I still don't know which version of the column filtering
> patch should I look at ... maybe there's none up to date, at the moment?

I don't think there is one.  I think the latest is what I posted in
https://postgr.es/m/[email-masked] (At least I
don't see any reply from Rahila with attachments after that), but that
wasn't addressing a bunch of review comments that had been made; and I
suspect that Amit K has already committed a few conflicting patches
after that.

> Should be "FOR ABSOLUTELY EVERYTHING IN SCHEMA" of course ;-)

hahah ...

> best / most flexible choice, i.e. "FOR TABLES, SEQUENCES IN SCHEMA"?

Hmm, not sure if bison is going to like that.  Maybe it's OK if
SEQUENCES is a fully reserved word?  But nothing beats experimentation!

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
Thou shalt check the array bounds of all strings (indeed, all arrays), for
surely where thou typest "foo" someone someday shall type
"supercalifragilisticexpialidocious" (5th Commandment for C programmers)

---

From: "[email-masked]" <[email-masked]>
Date: Sat, 25 Sep 2021 01:54:12 +0000

From Fri, Sep 24, 2021 9:25 PM Alvaro Herrera <[email-masked]> wrote:
> Yeah, I think pubobj_name remains a problem in that you don't know its return
> type -- could be a String or a RangeVar, and the user of that production can't
> distinguish.  So you're still (unnecessarily, IMV) stashing an object of
> undetermined type into ->object.
> 

Hi,

IIRC, the above grammar doesn't support extended relation expression (like:
"tablename * ", "ONLY tablename", "ONLY '( tablename )") which is part of rule
relation_expr. I think we should add these too. And if we move forward with the
design you proposed, we should do something like the following:

			TABLE relation_expr
								$$->rangevar = $2;
								$$->name = $5;
			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
								$$->name = $5;
			| extended_relation_expr	/* grammar like tablename * , ONLY tablename, ONLY ( tablename )*/

							}
			| ColId indirection
							{

							}

Best regards,
Hou zj

---

From: vignesh C <[email-masked]>
Date: Sat, 25 Sep 2021 13:15:08 +0530

>                                                 $$->pubobjtype = PUBLICATIONOBJ_CURRSCHEMA;

Apart from the issue that Hou San pointed, I found one issue with
introduction of PUBLICATIONOBJ_CURRSCHEMA, I was not able to
differentiate if it is table or schema in the following cases:
CREATE PUBLICATION pub1 FOR ALL TABLES IN SCHEMA CURRENT_SCHEMA;
CREATE PUBLICATION pub1 FOR ALL TABLES IN SCHEMA sch1, CURRENT_SCHEMA;
CREATE PUBLICATION pub1 FOR table t1, CURRENT_SCHEMA;
The differentiation is required to differentiate and add a schema or a table.

I felt it was better to use PUBLICATIONOBJ_CONTINUATION in case of
CURRENT_SCHEMA in multiple object cases like:
PublicationObjSpec:     TABLE relation_expr
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->pubobjtype =
PUBLICATIONOBJ_TABLE;
                                                $$->rangevar = $2;
                                        }
                        | ALL TABLES IN_P SCHEMA ColId
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->pubobjtype =
PUBLICATIONOBJ_REL_IN_SCHEMA;
                                                $$->name = $5;
                                                $$->location = @5;
                                        }
                        | ALL TABLES IN_P SCHEMA CURRENT_SCHEMA /* XXX
should this be "IN_P CURRENT_SCHEMA"? */
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->pubobjtype =
PUBLICATIONOBJ_REL_IN_SCHEMA;
                                                $$->name = "CURRENT_SCHEMA";
                                                $$->location = @5;
                                        }
                        | ColId
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->name = $1;
                                                $$->pubobjtype =
PUBLICATIONOBJ_CONTINUATION;
                                                $$->location = @1;
                                        }
                        | ColId indirection
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->rangevar =
makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
                                                $$->pubobjtype =
PUBLICATIONOBJ_CONTINUATION;
                                                $$->location = @1;
                                        }
                        | CURRENT_SCHEMA
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->pubobjtype =
PUBLICATIONOBJ_CONTINUATION;
                                                $$->name = "CURRENT_SCHEMA";
                                                $$->location = @1;
                                        }
                        | extended_relation_expr        /* grammar
like tablename * , ONLY tablename, ONLY ( tablename )*/
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                /*$$->rangevar =
makeRangeVarFromQualifiedName($1, $2, @1, yyscanner); */
                                                $$->rangevar = $1;
                                                $$->pubobjtype =
PUBLICATIONOBJ_CONTINUATION;
                                        }
                                ;

I'm ok with your suggestion along with the above proposed changes. I
felt the changes proposed at [1] were also fine. Let's change it to
whichever is better, easily extendable and can handle the Column
filtering project, ALL TABLES IN SCHEMA, ALL SEQUENCES IN SCHEMA
projects, and other projects in the future. Based on that we can check
in the parser changes independently and then the remaining series of
the patches can be rebased on top of it accordingly. Thoughts?

> the code in ObjectsInPublicationToOids less messy.

I agree with this. I will make the changes for this in the next version.

> Did you try having a schema named CURRENT_SCHEMA?)

Here CURRENT_SCHEMA is not used for the schema name, it will be
replaced with the name of the schema that is first in the search path.

Regards,
Vignesh

---

From: Amit Kapila <[email-masked]>
Date: Mon, 27 Sep 2021 16:40:50 +0530

> The differentiation is required to differentiate and add a schema or a table.

I am not sure what makes you say that we can't distinguish the above
cases when there is already a separate rule for CURRENT_SCHEMA? I
think you can distinguish by tracking the previous objects as we are
already doing in the patch. But one thing that is not clear to me is
is the reason to introduce a new type PUBLICATIONOBJ_CURRSCHEMA when
we use PUBLICATIONOBJ_REL_IN_SCHEMA and PUBLICATIONOBJ_CONTINUATION to
distinguish all cases of CURRENT_SCHEMA. Alvaro might have something
in mind for this which is not apparent and that might have caused
confusion to you as well?

-- 
With Regards,
Amit Kapila.

---

From: vignesh C <[email-masked]>
Date: Mon, 27 Sep 2021 17:13:55 +0530

> confusion to you as well?

It is difficult to identify this case:
1) create publication pub1 for all tables in schema CURRENT_SCHEMA;
2) create publication pub1 for CURRENT_SCHEMA;

Here case 1 should succeed and case 2 should throw error:
Since the object type will be set to PUBLICATIONOBJ_CURRSCHEMA in both
cases, we cannot differentiate between them:
1) ALL TABLES IN_P SCHEMA CURRENT_SCHEMA /* XXX should this be "IN_P
CURRENT_SCHEMA"? */
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->pubobjtype =
PUBLICATIONOBJ_CURRSCHEMA;
                                                $$->name = $4;
                                        }
2) CURRENT_SCHEMA
                                        {
                                                $$ =
makeNode(PublicationObjSpec);
                                                $$->pubobjtype =
PUBLICATIONOBJ_CURRSCHEMA;
                                        }

I felt it will work, if we set object type to
PUBLICATIONOBJ_CONTINUATION in 2nd case(CURRENT_SCHEMA) and setting
object type to PUBLICATIONOBJ_REL_IN_SCHEMA or
PUBLICATIONOBJ_CURRSCHEMA in 1st case( ALL TABLES IN_P SCHEMA
CURRENT_SCHEMA).
Thoughts?

Regards,
Vignesh

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 27 Sep 2021 09:23:49 -0300

> confusion to you as well?

My issue is what happens if you have a schema that is named
CURRENT_SCHEMA.  In the normal case where you do ALL TABLES IN SCHEMA
"CURRENT_SCHEMA" you would end up with a String containing
"CURRENT_SCHEMA", so how do you distinguish that from ALL TABLES IN
SCHEMA CURRENT_SCHEMA, which does not refer to the schema named
"CURRENT_SCHEMA" but in Vignesh's proposal also uses a String containing
"CURRENT_SCHEMA"?

Now you could say "but who would be stupid enough to do that??!", but it
seems easier to dodge the problem entirely.  AFAICS our grammar never
uses String "CURRENT_SCHEMA" to represent CURRENT_SCHEMA, but rather
some special enum value.

---

From: Rahila Syed <[email-masked]>
Date: Mon, 27 Sep 2021 18:40:57 +0530

Hi,

> Yes, the v5 version of the patch attached by Alvaro is the latest one.
IIUC, the review comments that are yet to be addressed apart from the
ongoing grammar
discussion, are as follows:

1. Behaviour on dropping a column from the table, that is a part of column
filter.
In the latest patch, the entire table is dropped from publication on
dropping a column
that is a part of the column filter. However, there is preference for
another approach
to drop just the column from the filter on DROP column CASCADE(continue to
filter
the other columns), and an error for DROP RESTRICT.

2. Instead of WITH RECURSIVE query to find the topmost parent of the
partition
in fetch_remote_table_info, use pg_partition_tree and pg_partition_root.

3. Report of memory leakage in get_rel_sync_entry().

4. Missing documentation

5. Latest comments(last two messages) by Peter Smith.

Thank you,
Rahila Syed

---

From: Amit Kapila <[email-masked]>
Date: Tue, 28 Sep 2021 08:59:51 +0530

On Mon, Sep 27, 2021 at 5:53 PM Alvaro Herrera <[email-masked]> wrote:
> Now you could say "but who would be stupid enough to do that??!",
>

But it is not allowed to create schema or table with the name
CURRENT_SCHEMA, so not sure if we need to do anything special for it.
However, if we want to handle it as a separate enum then the handling
would be something like:

Now, during post-processing, the PUBLICATIONOBJ_CONTINUATION will be
distinguished as CURRENT_SCHEMA because both rangeVar and name will be
NULL. Do you have other ideas to deal with it? Vignesh has already
point in his email [1] why we can't keep pubobjtype as
PUBLICATIONOBJ_CURRSCHEMA in the second case, so I used
PUBLICATIONOBJ_CONTINUATION.

[1] - https://www.postgresql.org/message-id/CALDaNm06shp%2BALwC2s-dV-S4k2o6bcmXnXGX4ETkoXxKHQfjfA%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Tue, 28 Sep 2021 09:31:33 +0530

On Mon, Sep 27, 2021 at 6:41 PM Rahila Syed <[email-masked]> wrote:
> IIUC, the review comments that are yet to be addressed apart from the ongoing grammar
> 1. Behaviour on dropping a column from the table, that is a part of column filter.
> In the latest patch, the entire table is dropped from publication on dropping a column
> that is a part of the column filter. However, there is preference for another approach
> to drop just the column from the filter on DROP column CASCADE(continue to filter

I am not sure if we can do this as pointed by me in one of the
previous emails [1]. I think additionally, you might want to take some
action if the replica identity is changed as requested in the same
email [1].

[1] - https://www.postgresql.org/message-id/CAA4eK1KCGF43pfLv8%2BmixcTMs%3DNkd6YdWL53LhiT1DvnuTg01g%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 29 Sep 2021 10:19:35 -0300

> CURRENT_SCHEMA, so not sure if we need to do anything special for it.

Oh?  You certainly can.

alvherre=# create schema "CURRENT_SCHEMA";
CREATE SCHEMA
alvherre=# \dn
        Listado de esquemas
     Nombre     |       Dueño       
----------------+-------------------
 CURRENT_SCHEMA | alvherre
 public         | pg_database_owner
 temp           | alvherre
(3 filas)

alvherre=# create table "CURRENT_SCHEMA"."CURRENT_SCHEMA" ("bother amit for a while" int);
CREATE TABLE
alvherre=# \d "CURRENT_SCHEMA".*
                  Tabla «CURRENT_SCHEMA.CURRENT_SCHEMA»
         Columna         |  Tipo   | Ordenamiento | Nulable | Por omisión 
-------------------------+---------+--------------+---------+-------------
 bother amit for a while | integer |              |         | 

> NULL. Do you have other ideas to deal with it?

That sounds plausible.  There's no need for a name-free object of any other
kind AFAICS, so there should be no conflict.  If we ever do find a
conflict, we can add another struct member to disambiguate.

Thanks

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
"Doing what he did amounts to sticking his fingers under the hood of the
implementation; if he gets his fingers burnt, it's his problem."  (Tom Lane)

---

From: Amit Kapila <[email-masked]>
Date: Thu, 30 Sep 2021 16:39:41 +0530

> alvherre=# create schema "CURRENT_SCHEMA";

oops, I was trying without quotes.

> conflict, we can add another struct member to disambiguate.

Okay, thanks. I feel now we are in agreement on the grammar rules.

-- 
With Regards,
Amit Kapila.

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 1 Dec 2021 17:29:47 -0300

Hi

I took the latest posted patch, rebased on current sources, fixed the
conflicts, and pgindented.  No further changes.  Here's the result.  All
tests are passing for me.  Some review comments that were posted have
not been addressed yet; I'll look into that soon.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Java is clearly an example of money oriented programming"  (A. Stepanov)

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 1 Dec 2021 17:47:07 -0300

Oh, I just noticed that for some reason the test file was lost in the
rebase, so those tests I thought I was running ... I wasn't.  And of
course if I put it back, it fails.

More later.

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/
"Crear es tan difícil como ser libre" (Elsa Triolet)

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 2 Dec 2021 11:23:58 -0300

> not been addressed yet; I'll look into that soon.

In v7 I have reinstated the test file and fixed the silly problem that
caused it to fail (probably a mistake of mine while rebasing).

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
Maybe there's lots of data loss but the records of data loss are also lost.
(Lincoln Yeoh)

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 2 Dec 2021 16:15:03 -0300

> Was that omission deliberate?

Somehow I not only failed to notice the omission, but also your email
where you told us about it.  I have since posted a version of the patch
that again includes it.

Thanks!

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"No renuncies a nada. No te aferres a nada."

---

From: vignesh C <[email-masked]>
Date: Fri, 3 Dec 2021 14:47:39 +0530

> where you told us about it.  I have since posted a version of the patch

Thanks for the patch, Few comments:
I had a look at the patch, I felt the following should be handled:
1) Dump changes to include the column filters while adding table to
publication in dumpPublicationTable
2) Documentation changes for column filtering in create_publication.sgml
3) describe publication changes to support \dRp command in describePublications
4) I felt we need not allow specifying columns in case of "alter
publication drop table" as currently dropping column filter is not
allowed.
5) We should check if the column specified is present in the table,
currently we are able to specify non existent column for column
filtering
+       foreach(lc, targetrel->columns)
+       {
+               char       *colname;
+
+               colname = strVal(lfirst(lc));
+               target_cols = lappend(target_cols, colname);
+       }
+       check_publication_add_relation(targetrel->relation, target_cols);

Regards,
Vignesh

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 10 Dec 2021 14:08:52 +0100

> caused it to fail (probably a mistake of mine while rebasing).

I looked through this a bit.  You had said that you are still going to 
integrate past review comments, so I didn't look to deeply before you 
get to that.

Attached are a few fixup patches that you could integrate.

There was no documentation, so I wrote a bit (patch 0001).  It only 
touches the CREATE PUBLICATION and ALTER PUBLICATION pages at the 
moment.  There was no mention in the Logical Replication chapter that 
warranted updating.  Perhaps we should revisit that chapter at the end 
of the release cycle.

DDL tests should be done in src/test/regress/sql/publication.sql rather 
than through TAP tests, to keep it simpler.  I have added a few that I 
came up with (patch 0002).  Note the FIXME marker that it does not 
recognize if the listed columns don't exist.  I removed a now redundant 
test from the TAP test file.  The other error condition test in the TAP 
test file ('publication relation test_part removed') I didn't 
understand: test_part was added with columns (a, b), so why would 
dropping column b remove the whole entry?  Maybe I missed something, or 
this could be explained better.

I was curious what happens when you have different publications with 
different column lists, so I wrote a test for that (patch 0003).  It 
turns out it works, so there is nothing to do, but perhaps the test is 
useful to keep.

The test file 021_column_filter.pl should be renamed to an unused number 
(would be 027 currently).  Also, it contains references to "TRUNCATE", 
where it was presumably copied from.

On the implementation side, I think the added catalog column 
pg_publication_rel.prattrs should be an int2 array, not a text array. 
That would also fix the above problem.  If you have to look up the 
columns at DDL time, then you will notice when they don't exist.

Finally, I suggest not naming this feature "column filter".  I think 
this name arose because of the analogy with the "row filter" feature 
also being developed.  But a filter is normally a dynamic data-driven 
action, which this is not.  Golden Gate calls it in their documentation 
"Selecting Columns", or we could just call it "column list".

From cc9082fe6f98e5d9d992377761d06b8aadf3b27f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Fri, 10 Dec 2021 13:19:52 +0100
Subject: [PATCH 1/3] Add some documentation

---
 doc/src/sgml/ref/alter_publication.sgml  |  4 +++-
 doc/src/sgml/ref/create_publication.sgml | 11 ++++++++++-
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index bb4ef5e5e2..c86055b93c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -30,7 +30,7 @@
 
 <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>
@@ -110,6 +110,8 @@ <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.
+      Optionally, a column list can be specified.  See <xref
+      linkend="sql-createpublication"/> for details.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index d805e8e77a..73a23cbb02 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@
 
 <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 @@ <title>Parameters</title>
       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,
-- 
2.34.1

From dadc41b139c352811f5956892c02135905263347 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Fri, 10 Dec 2021 13:40:50 +0100
Subject: [PATCH 2/3] Add regression tests

---
 src/test/regress/expected/publication.out    | 16 +++++++++++++++-
 src/test/regress/sql/publication.sql         | 12 +++++++++++-
 src/test/subscription/t/021_column_filter.pl |  6 +-----
 3 files changed, 27 insertions(+), 7 deletions(-)

diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5ac2d666a2..c6017c0514 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,7 +165,21 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
-DROP TABLE testpub_tbl2;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (x, y, z);  -- error
+ERROR:  cannot add relation "testpub_tbl5" to publication
+DETAIL:  Column filter must include REPLICA IDENTITY columns
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);  -- error FIXME
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);  -- error
+ERROR:  relation "testpub_tbl5" is already member of publication "testpub_fortable"
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);  -- ok
+ERROR:  relation "testpub_tbl5" is already member of publication "testpub_fortable"
+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:  cannot add relation "testpub_tbl6" to publication
+DETAIL:  Cannot have column filter with REPLICA IDENTITY FULL
+DROP TABLE testpub_tbl2, testpub_tbl5, testpub_tbl6;
 DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 56dd358554..b2fd793c61 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,7 +89,17 @@ CREATE PUBLICATION testpub_for_tbl_schema FOR ALL TABLES IN SCHEMA pub_test, TAB
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
-DROP TABLE testpub_tbl2;
+CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text);
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (x, y, z);  -- error
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);  -- error FIXME
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (b, c);  -- error
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, c);  -- ok
+
+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
+
+DROP TABLE testpub_tbl2, testpub_tbl5, testpub_tbl6;
 DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
 
 CREATE TABLE testpub_tbl3 (a int);
diff --git a/src/test/subscription/t/021_column_filter.pl b/src/test/subscription/t/021_column_filter.pl
index 7b52d4593d..02e05420f1 100644
--- a/src/test/subscription/t/021_column_filter.pl
+++ b/src/test/subscription/t/021_column_filter.pl
@@ -5,7 +5,7 @@
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 9;
+use Test::More tests => 8;
 
 # setup
 
@@ -130,10 +130,6 @@
 is($result, qq(1|abc), 'update on column c is not replicated');
 
 #Test error conditions
-my ($psql_rc, $psql_out, $psql_err) = $node_publisher->psql('postgres',
-	"CREATE PUBLICATION pub2 FOR TABLE test_part(b)");
-like($psql_err, qr/Column filter must include REPLICA IDENTITY columns/, 'Error when column filter does not contain REPLICA IDENTITY');
-
 $node_publisher->safe_psql('postgres',
 	"ALTER TABLE test_part DROP COLUMN b");
 $result = $node_publisher->safe_psql('postgres',
-- 
2.34.1

From 8f8307000fcd047f3a11ddc4366e6fd386908296 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Fri, 10 Dec 2021 13:51:38 +0100
Subject: [PATCH 3/3] Test case for overlapping column lists

---
 src/test/subscription/t/021_column_filter.pl | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/test/subscription/t/021_column_filter.pl b/src/test/subscription/t/021_column_filter.pl
index 02e05420f1..3814ad2367 100644
--- a/src/test/subscription/t/021_column_filter.pl
+++ b/src/test/subscription/t/021_column_filter.pl
@@ -5,7 +5,7 @@
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 8;
+use Test::More tests => 9;
 
 # setup
 
@@ -137,3 +137,18 @@
 is($result, qq(tab1|{a,B}
 tab2|{a,b}
 tab3|{a',c'}), 'publication relation test_part removed');
+
+
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab4 (a int PRIMARY KEY, b int, c int, d int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab4 (a int PRIMARY KEY, b int, d int)");
+$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub2 FOR TABLE tab4 (a, b)");
+$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub3 FOR TABLE tab4 (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 tab4 VALUES (1, 11, 111, 1111)");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2, 22, 222, 2222)");
+$node_publisher->wait_for_catchup('sub2');
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab4;"),
+	qq(1|11|1111
+2|22|2222),
+	'overlapping publications with overlapping column lists');
-- 
2.34.1

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 10 Dec 2021 10:23:51 -0300

On 2021-Dec-10, Peter Eisentraut wrote:

> integrate past review comments, so I didn't look to deeply before you get to
> that.

Thanks for doing this!  As it happens I've spent the last couple of days
working on some of these details.

> There was no documentation, so I wrote a bit (patch 0001).  It only touches
> the CREATE PUBLICATION and ALTER PUBLICATION pages at the moment.  There was
> no mention in the Logical Replication chapter that warranted updating.
> Perhaps we should revisit that chapter at the end of the release cycle.

Thanks.  I hadn't looked at the docs yet, so I'll definitely take this.

> DDL tests should be done in src/test/regress/sql/publication.sql rather than
> through TAP tests, to keep it simpler.

Yeah, I noticed this too but hadn't done it yet.

> Note the FIXME marker that it does not recognize if the
> listed columns don't exist.

I had fixed this already, so I suppose it should be okay.

> I removed a now redundant test from the TAP
> test file.  The other error condition test in the TAP test file
> ('publication relation test_part removed') I didn't understand: test_part
> was added with columns (a, b), so why would dropping column b remove the
> whole entry?  Maybe I missed something, or this could be explained better.

There was some discussion about it earlier in the thread and I was also
against this proposed behavior.

> different column lists, so I wrote a test for that (patch 0003).  It turns
> out it works, so there is nothing to do, but perhaps the test is useful to
> keep.

Great, thanks.  Yes, I think it will be.

> pg_publication_rel.prattrs should be an int2 array, not a text array.

I already rewrote it to use a int2vector column in pg_publication_rel.
This interacted badly with the previous behavior on dropping columns,
which I have to revisit, but otherwise it seems much better.
(Particularly since we don't need to care about quoting names and such.)

> Finally, I suggest not naming this feature "column filter".  I think this
> name arose because of the analogy with the "row filter" feature also being
> developed.  But a filter is normally a dynamic data-driven action, which
> this is not.  Golden Gate calls it in their documentation "Selecting
> Columns", or we could just call it "column list".

Hmm, I hadn't thought of renaming the feature, but I have to admit that
I was confused because of the name, so I agree with choosing some other
name.

I'll integrate your changes and post the whole thing later.

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/
Si no sabes adonde vas, es muy probable que acabes en otra parte.

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 10 Dec 2021 17:02:08 -0300

> the drop.  Then it is up to them to figure out what to do.

I thought about this some more and realized that our earlier conclusions
were wrong or at least inconvenient.  I think that the best behavior if
you drop a column from a table is to remove the column from the
publication column list, and do nothing else.

Consider the case where you add a table to a publication without a
column filter, and later drop the column.  You don't get an error that
the relation is part of a publication; simply, the subscribers of that
publication will no longer receive that column.

Similarly for this case: if you add a table to a publication with a
column list, and later drop a column in that list, then you shouldn't
get an error either.  Simply the subscribers of that publication should
receive one column less.

Should be fairly quick to implement ... on it now.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"The problem with the facetime model is not just that it's demoralizing, but
that the people pretending to work interrupt the ones actually working."
                                                           (Paul Graham)

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 10 Dec 2021 18:08:24 -0300

> Should be fairly quick to implement ... on it now.

Actually it's not so easy to implement.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"No hay ausente sin culpa ni presente sin disculpa" (Prov. francés)

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 13 Dec 2021 14:44:11 -0300

> Actually it's not so easy to implement.

So I needed to add "sub object id" support for pg_publication_rel
objects in pg_depend / dependency.c.  What I have now is partial (the
describe routines need patched) but it's sufficient to show what's
needed.  In essence, we now set these depend entries with column
numbers, so that they can be dropped independently; when the drop comes,
the existing pg_publication_rel row is modified to cover the remaining
columns.  As far as I can tell, it works correctly.

There is one policy decision to make: what if ALTER TABLE drops the last
remaining column in the publication?  I opted to raise a specific error
in this case, though we could just the same opt to drop the relation
from the publication.  Are there opinions on this?

This version incorporates the fixups Peter submitted, plus some other
fixes of my own.  Notably, as Peter also mentioned, I changed
pg_publication_rel.prattrs to store int2vector rather than an array of
column names.  This makes for better behavior if columns are renamed and
things like that, and also we don't need to be so cautious about
quoting.  It does mean we need a slightly more complicated query in a
couple of spots, but that should be okay.

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/
"Always assume the user will do much worse than the stupidest thing
you can imagine."                                (Julien PUYDT)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index fe9c714257..a88d12e8ae 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1472,7 +1472,7 @@ doDeletion(const ObjectAddress *object, int flags)
 			break;
 
 		case OCLASS_PUBLICATION_REL:
-			RemovePublicationRelById(object->objectId);
+			RemovePublicationRelById(object->objectId, object->objectSubId);
 			break;
 
 		case OCLASS_PUBLICATION:
@@ -2754,8 +2754,12 @@ free_object_addresses(ObjectAddresses *addrs)
 ObjectClass
 getObjectClass(const ObjectAddress *object)
 {
-	/* only pg_class entries can have nonzero objectSubId */
+	/*
+	 * only pg_class and pg_publication_rel entries can have nonzero
+	 * objectSubId
+	 */
 	if (object->classId != RelationRelationId &&
+		object->classId != PublicationRelRelationId &&
 		object->objectSubId != 0)
 		elog(ERROR, "invalid non-zero objectSubId for object class %u",
 			 object->classId);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2bae3fbb17..5eed248dcb 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -4019,6 +4019,7 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				/* translator: first %s is, e.g., "table %s" */
 				appendStringInfo(&buffer, _("publication of %s in publication %s"),
 								 rel.data, pubname);
+				/* FIXME add objectSubId support */
 				pfree(rel.data);
 				ReleaseSysCache(tup);
 				break;
@@ -5853,9 +5854,16 @@ getObjectIdentityParts(const ObjectAddress *object,
 
 				getRelationIdentity(&buffer, prform->prrelid, objname, false);
 				appendStringInfo(&buffer, " in publication %s", pubname);
+				if (object->objectSubId)	/* FIXME maybe get_attname */
+					appendStringInfo(&buffer, " column %d", object->objectSubId);
 
 				if (objargs)
+				{
 					*objargs = list_make1(pubname);
+					if (object->objectSubId)
+						*objargs = lappend(*objargs,
+										   psprintf("%d", object->objectSubId));
+				}
 
 				ReleaseSysCache(tup);
 				break;
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 07bcdc463a..462c8efe70 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -658,6 +658,56 @@ isObjectPinned(const ObjectAddress *object)
  * Various special-purpose lookups and manipulations of pg_depend.
  */
 
+/*
+ * Find all objects of the given class that reference the specified object,
+ * and add them to the given ObjectAddresses.
+ */
+void
+findAndAddAddresses(ObjectAddresses *addrs, Oid classId,
+					Oid refclassId, Oid refobjectId, int32 refobjsubId)
+{
+	Relation	depRel;
+	ScanKeyData	key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(refclassId));
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(refobjectId));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_INT4EQ,
+				Int32GetDatum(refobjsubId));
+
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform = (Form_pg_depend) GETSTRUCT(tup);
+		ObjectAddress	object;
+
+		if (depform->classid != classId)
+			continue;
+
+		ObjectAddressSubSet(object, depform->classid, depform->objid,
+							depform->refobjsubid);
+
+		add_exact_object_address(&object, addrs);
+	}
+
+	systable_endscan(scan);
+
+	table_close(depRel, AccessShareLock);
+}
+
 
 /*
  * Find the extension containing the specified object, if any
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a5229aea51..b99b3748c9 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -328,6 +328,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	{
 		table_close(rel, RowExclusiveLock);
 
+		/* FIXME need to handle the case of different column list */
+
 		if (if_not_exists)
 			return InvalidObjectAddress;
 
@@ -395,12 +397,6 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 
 	ObjectAddressSet(myself, PublicationRelRelationId, prrelid);
 
-	while ((attnum = bms_first_member(attmap)) >= 0)
-	{
-		/* Add dependency on the column */
-		ObjectAddressSubSet(referenced, RelationRelationId, relid, attnum);
-		recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
-	}
 	/* Add dependency on the publication */
 	ObjectAddressSet(referenced, PublicationRelationId, pubid);
 	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
@@ -409,6 +405,21 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	ObjectAddressSet(referenced, RelationRelationId, relid);
 	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
 
+	/*
+	 * If there's an explicit column list, make one dependency entry for each
+	 * column.  Note that the referencing side of the dependency is also
+	 * specific to one column, so that it can be dropped separately if the
+	 * column is dropped.
+	 */
+	while ((attnum = bms_first_member(attmap)) >= 0)
+	{
+		ObjectAddressSubSet(referenced, RelationRelationId, relid,
+							attnum + FirstLowInvalidHeapAttributeNumber);
+		myself.objectSubId = attnum + FirstLowInvalidHeapAttributeNumber;
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+	myself.objectSubId = 0;		/* need to undo this bit */
+
 	/* Close the table. */
 	table_close(rel, RowExclusiveLock);
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 753df44613..0078c5986c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -561,7 +561,6 @@ 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);
 			}
@@ -758,10 +757,11 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 }
 
 /*
- * Remove relation from publication by mapping OID.
+ * Remove relation from publication by mapping OID, or publication status
+ * of one column of that relation in the publication if an attnum is given.
  */
 void
-RemovePublicationRelById(Oid proid)
+RemovePublicationRelById(Oid proid, int32 attnum)
 {
 	Relation	rel;
 	HeapTuple	tup;
@@ -791,7 +791,81 @@ RemovePublicationRelById(Oid proid)
 
 	InvalidatePublicationRels(relids);
 
-	CatalogTupleDelete(rel, &tup->t_self);
+	/*
+	 * If no column is given, simply delete the relation from the publication.
+	 *
+	 * If a column is given, what we do instead is to remove that column from
+	 * the column list.  The relation remains in the publication, with the
+	 * other columns.  However, dropping the last column is disallowed.
+	 */
+	if (attnum == 0)
+	{
+		CatalogTupleDelete(rel, &tup->t_self);
+	}
+	else
+	{
+		Datum		adatum;
+		ArrayType  *arr;
+		int			nelems;
+		int16	   *elems;
+		int16	   *newelems;
+		int2vector *newvec;
+		Datum		values[Natts_pg_publication_rel];
+		bool		nulls[Natts_pg_publication_rel];
+		bool		replace[Natts_pg_publication_rel];
+		HeapTuple	newtup;
+		int			i,
+					j;
+		bool		isnull;
+
+		/* Obtain the original column list */
+		adatum = SysCacheGetAttr(PUBLICATIONRELMAP,
+								 tup,
+								 Anum_pg_publication_rel_prattrs,
+								 &isnull);
+		if (isnull)			/* shouldn't happen */
+			elog(ERROR, "can't drop column from publication without a column list");
+		arr = DatumGetArrayTypeP(adatum);
+		nelems = ARR_DIMS(arr)[0];
+		elems = (int16 *) ARR_DATA_PTR(arr);
+
+		/* Construct a list excluding the given column */
+		newelems = palloc(sizeof(int16) * nelems - 1);
+		for (i = 0, j = 0; i < nelems - 1; i++)
+		{
+			if (elems[i] == attnum)
+				continue;
+			newelems[j++] = elems[i];
+		}
+
+		/*
+		 * If this is the last column used in the publication, disallow the
+		 * command. We could alternatively just drop the relation from the
+		 * publication.
+		 */
+		if (j == 0)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot drop the last column in publication \"%s\"",
+						   get_publication_name(pubrel->prpubid, false)),
+					errhint("Remove table \"%s\" from the publication first.",
+							get_rel_name(pubrel->prrelid)));
+		}
+
+		/* Build the updated tuple */
+		MemSet(values, 0, sizeof(values));
+		MemSet(nulls, false, sizeof(nulls));
+		MemSet(replace, false, sizeof(replace));
+		newvec = buildint2vector(newelems, j);
+		values[Anum_pg_publication_rel_prattrs - 1] = PointerGetDatum(newvec);
+		replace[Anum_pg_publication_rel_prattrs - 1] = true;
+
+		/* Execute the update */
+		newtup = heap_modify_tuple(tup, RelationGetDescr(rel),
+								   values, nulls, replace);
+		CatalogTupleUpdate(rel, &tup->t_self, newtup);
+	}
 
 	ReleaseSysCache(tup);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c821271306..705bddc773 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -40,8 +40,9 @@
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
-#include "catalog/pg_tablespace.h"
+#include "catalog/pg_publication_rel.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -8415,6 +8416,13 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	/*
+	 * If the column is part of a replication column list, arrange to get that
+	 * removed too.
+	 */
+	findAndAddAddresses(addrs, PublicationRelRelationId,
+						RelationRelationId, RelationGetRelid(rel), attnum);
+
 	/*
 	 * 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
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index dfb5d0430c..f9f9ecd0c0 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -621,6 +621,8 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		 * filter. XXX Allow sending type information for REPLICA IDENTITY
 		 * COLUMNS with user created type. even when they are not mentioned in
 		 * column filters.
+		 *
+		 * FIXME -- this code seems not verified by tests.
 		 */
 		if (att_map != NULL &&
 			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2f412ca3db..84ee807e0b 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1648,6 +1648,8 @@ psql_completion(const char *text, int start, int end)
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3eca295ff4..76d421e09e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -214,6 +214,9 @@ extern long changeDependenciesOf(Oid classId, Oid oldObjectId,
 extern long changeDependenciesOn(Oid refClassId, Oid oldRefObjectId,
 								 Oid newRefObjectId);
 
+extern void findAndAddAddresses(ObjectAddresses *addrs, Oid classId,
+					Oid refclassId, Oid refobjectId, int32 refobjsubId);
+
 extern Oid	getExtensionOfObject(Oid classId, Oid objectId);
 extern List *getAutoExtensionsOfObject(Oid classId, Oid objectId);
 
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 4ba68c70ee..23f037df7f 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -25,7 +25,7 @@
 extern ObjectAddress CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt);
 extern void AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt);
 extern void RemovePublicationById(Oid pubid);
-extern void RemovePublicationRelById(Oid proid);
+extern void RemovePublicationRelById(Oid proid, int32 attnum);
 extern void RemovePublicationSchemaById(Oid psoid);
 
 extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 93ef6e21eb..aef2f905a1 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -167,18 +167,32 @@ Publications:
 
 CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text);
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (x, y, z);  -- error
-ERROR:  cannot add relation "testpub_tbl5" to publication
-DETAIL:  Column filter must include REPLICA IDENTITY columns
-ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);  -- error FIXME
+ERROR:  column "x" of relation "testpub_tbl5" does not exist
+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:  relation "testpub_tbl5" is already member of publication "testpub_fortable"
+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, c);  -- ok
-ERROR:  relation "testpub_tbl5" is already member of publication "testpub_fortable"
+ALTER TABLE testpub_tbl5 DROP COLUMN c;
+\dRp+ testpub_fortable
+                                Publication testpub_fortable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "public.testpub_tbl5" (a)
+Tables from schemas:
+    "pub_test"
+
+ALTER TABLE testpub_tbl5 DROP COLUMN a;
+ERROR:  cannot drop the last column in publication "testpub_fortable"
+HINT:  Remove table "testpub_tbl5" from the publication first.
 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:  cannot add relation "testpub_tbl6" to publication
-DETAIL:  Cannot have column filter with REPLICA IDENTITY FULL
+ERROR:  invalid column list for publishing relation "testpub_tbl6"
+DETAIL:  Cannot have column filter on relations with REPLICA IDENTITY FULL.
 DROP TABLE testpub_tbl2, testpub_tbl5, testpub_tbl6;
 DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
 CREATE TABLE testpub_tbl3 (a int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index eb0e71ea62..18b87803c0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -91,9 +91,12 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 
 CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text);
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (x, y, z);  -- error
-ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);  -- error FIXME
+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, c);  -- ok
+ALTER TABLE testpub_tbl5 DROP COLUMN c;
+\dRp+ testpub_fortable
+ALTER TABLE testpub_tbl5 DROP COLUMN a;
 
 CREATE TABLE testpub_tbl6 (a int, b text, c text);
 ALTER TABLE testpub_tbl6 REPLICA IDENTITY FULL;
diff --git a/src/test/subscription/t/021_column_filter.pl b/src/test/subscription/t/021_column_filter.pl
index 27d0537621..354e6ac363 100644
--- a/src/test/subscription/t/021_column_filter.pl
+++ b/src/test/subscription/t/021_column_filter.pl
@@ -5,7 +5,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 8;
+use Test::More tests => 10;
 
 # setup
 
@@ -129,14 +129,23 @@ $node_publisher->safe_psql('postgres',
 	"UPDATE tab2 SET c = 5 where a = 1");
 is($result, qq(1|abc), 'update on column c is not replicated');
 
-# Test error conditions
+# Test behavior when a column is dropped
 $node_publisher->safe_psql('postgres',
 	"ALTER TABLE test_part DROP COLUMN b");
 $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
+	"select prrelid::regclass, prattrs from pg_publication_rel pb;");
+is($result,
+	q(tab1|1 2
+tab3|1 3
 tab2|1 2
-tab3|1 3), 'publication relation test_part removed');
+test_part|1), 'column test_part.b removed');
+
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_part VALUES (3, '2021-12-13 12:13:14')");
+$node_publisher->wait_for_catchup('sub1');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM test_part WHERE a = 3");
+is($result, "3|", 'only column a is replicated');
 
 $node_publisher->safe_psql('postgres', "CREATE TABLE tab4 (a int PRIMARY KEY, b int, c int, d int)");
 $node_subscriber->safe_psql('postgres', "CREATE TABLE tab4 (a int PRIMARY KEY, b int, d int)");

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 13 Dec 2021 14:47:50 -0300

Hmm, I messed up the patch file I sent.  Here's the complete patch.

@@ -30,7 +30,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r

@@ -110,6 +110,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>

@@ -78,6 +78,15 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
+     <para>

 		case OCLASS_PUBLICATION:

 				if (objargs)
+				{
 
 				ReleaseSysCache(tup);
 				break;
index 5f37bf6d10..dfcb450e61 100644
 
+/*
 
 /*
  * Find the extension containing the specified object, if any
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 62f10bcbd2..ae58adc8e5 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -46,12 +46,18 @@
 #include "utils/syscache.h"
 
 /*
- * 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 column specified as column filter
+ * (shifted by FirstLowInvalidHeapAttributeNumber), or NULL if no column
+ * filter was specified.
  */
 static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(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 +88,40 @@ check_publication_add_relation(Relation targetrel)
 				 errmsg("cannot add relation \"%s\" to publication",
 						RelationGetRelationName(targetrel)),
 				 errdetail("This operation is not supported for unlogged tables.")));
+
+	/*
+	 * Enforce that the column filter 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 (columns != NULL)
+	{
+		if (replidentfull)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("invalid column list for publishing relation \"%s\"",
+						   RelationGetRelationName(targetrel)),
+					errdetail("Cannot have column filter on relations with REPLICA IDENTITY FULL."));
+		else
+		{
+			Bitmapset  *idattrs;
+
+			idattrs = RelationGetIndexAttrBitmap(targetrel,
+												 INDEX_ATTR_BITMAP_IDENTITY_KEY);
+			if (!bms_is_subset(idattrs, 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."));
+
+			if (idattrs)
+				pfree(idattrs);
+		}
+	}
 }
 
 /*
@@ -289,9 +329,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	Oid			relid = RelationGetRelid(targetrel->relation);
 	Oid			prrelid;
 	Publication *pub = GetPublication(pubid);
+	Bitmapset  *attmap = NULL;
+	AttrNumber *attarray;
+	int			natts = 0;
+	int			attnum;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
+	ListCell   *lc;
 
 	rel = table_open(PublicationRelRelationId, RowExclusiveLock);
 
@@ -305,6 +350,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	{
 		table_close(rel, RowExclusiveLock);

@@ -314,7 +361,34 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 						RelationGetRelationName(targetrel->relation), pub->name)));
 	}
 
-	check_publication_add_relation(targetrel->relation);
+	attarray = palloc(sizeof(AttrNumber) * list_length(targetrel->columns));
+	foreach(lc, targetrel->columns)
+	{
+		char	   *colname = strVal(lfirst(lc));
+		AttrNumber	attnum = get_attnum(relid, colname);
+
+		if (attnum == InvalidAttrNumber)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+						   colname, RelationGetRelationName(targetrel->relation)));
+		if (attnum < 0)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot reference system column \"%s\" in publication column list",
+						   colname));
+
+		if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, attmap))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column \"%s\" specified twice in publication column list",
+						   colname));
+
+		attmap = bms_add_member(attmap, attnum - FirstLowInvalidHeapAttributeNumber);
+		attarray[natts++] = attnum;
+	}
+
+	check_publication_add_relation(targetrel->relation, attmap);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
@@ -327,6 +401,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);
 
@@ -344,6 +427,21 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	ObjectAddressSet(referenced, RelationRelationId, relid);
 	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
 
+	/*
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 404bb5d0c8..a070914bdd 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -561,7 +561,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 
 				pubrel = palloc(sizeof(PublicationRelInfo));
 				pubrel->relation = oldrel;
-
+				pubrel->columns = NIL;
 				delrels = lappend(delrels, pubrel);
 			}
 		}
@@ -757,10 +757,11 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 }
 
 /*
@@ -790,7 +791,81 @@ RemovePublicationRelById(Oid proid)

 
 	ReleaseSysCache(tup);
 
@@ -932,6 +1007,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);
 
@@ -965,8 +1042,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);
 			}
@@ -1074,6 +1154,12 @@ 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 may not be specified for relation \"%s\" in ALTER PUBLICATION ... SET/DROP command",
+						   RelationGetRelationName(pubrel->relation)));
+
 		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 47b29001d5..7207dcf9c0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -40,8 +40,9 @@
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
-#include "catalog/pg_tablespace.h"
+#include "catalog/pg_publication_rel.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -8420,6 +8421,13 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	/*
+	 * If the column is part of a replication column list, arrange to get that
+	 * removed too.
+	 */
+	findAndAddAddresses(addrs, PublicationRelRelationId,
+						RelationRelationId, RelationGetRelid(rel), attnum);
+
 	/*
 	 * 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
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index df0b747883..0ff4c1ceac 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4833,6 +4833,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 cb7ddd463c..d786a688ac 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2312,6 +2312,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 3d4dd43e47..4dad6fedfb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9742,12 +9742,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
 				{
@@ -9762,28 +9763,38 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE_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
 				{
@@ -17435,8 +17446,9 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 			{
 				/* convert it to PublicationTable */
 				PublicationTable *pubtable = makeNode(PublicationTable);
-				pubtable->relation = makeRangeVar(NULL, pubobj->name,
-												  pubobj->location);
+
+				pubtable->relation =
+					makeRangeVar(NULL, pubobj->name, pubobj->location);
 				pubobj->pubtable = pubtable;
 				pubobj->name = NULL;
 			}
@@ -17444,6 +17456,16 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLE_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 9f5bf4b639..15d8192238 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -29,9 +29,9 @@
 #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 *att_map);
 static void logicalrep_write_tuple(StringInfo out, Relation rel,
-								   HeapTuple tuple, bool binary);
+								   HeapTuple tuple, bool binary, Bitmapset *att_map);
 
 static void logicalrep_read_attrs(StringInfo in, LogicalRepRelation *rel);
 static void logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple);
@@ -398,7 +398,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 *att_map)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_INSERT);
 
@@ -410,7 +410,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, att_map);
 }
 
 /*
@@ -442,7 +442,7 @@ 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 *att_map)
 {
 	pq_sendbyte(out, LOGICAL_REP_MSG_UPDATE);
 
@@ -463,11 +463,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, att_map);
 	}
 
 	pq_sendbyte(out, 'N');		/* new tuple follows */
-	logicalrep_write_tuple(out, rel, newtuple, binary);
+	logicalrep_write_tuple(out, rel, newtuple, binary, att_map);
 }
 
 /*
@@ -536,7 +536,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 +651,7 @@ 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 *att_map)
 {
 	char	   *relname;
 
@@ -673,7 +673,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, att_map);
 }
 
 /*
@@ -749,20 +749,42 @@ 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 *att_map)
 {
 	TupleDesc	desc;
 	Datum		values[MaxTupleAttributeNumber];
 	bool		isnull[MaxTupleAttributeNumber];
 	int			i;
 	uint16		nliveatts = 0;
+	Bitmapset  *idattrs = NULL;
+	bool		replidentfull;
+	Form_pg_attribute att;
 
 	desc = RelationGetDescr(rel);
 
+	replidentfull = (rel->rd_rel->relreplident == REPLICA_IDENTITY_FULL);
+	if (!replidentfull)
+		idattrs = RelationGetIdentityKeyBitmap(rel);
+
 	for (i = 0; i < desc->natts; i++)
 	{
+		att = TupleDescAttr(desc, i);
 		if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
 			continue;
+
+		/*
+		 * Do not increment count of attributes if not a part of column
+		 * filters except for replica identity columns or if replica identity
+		 * is full.
+		 */
+		if (att_map != NULL &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   att_map) &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   idattrs) &&
+			!replidentfull)
+			continue;
 		nliveatts++;
 	}
 	pq_sendint16(out, nliveatts);
@@ -800,6 +822,19 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple, bool binar
 			continue;
 		}
 
+		/*
+		 * Do not send attribute data if it is not a part of column filters,
+		 * except if it is a part of REPLICA IDENTITY or REPLICA IDENTITY is
+		 * full, send the data.
+		 */
+		if (att_map != NULL &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   att_map) &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   idattrs) &&
+			!replidentfull)
+			continue;
+
 		typtup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(att->atttypid));
 		if (!HeapTupleIsValid(typtup))
 			elog(ERROR, "cache lookup failed for type %u", att->atttypid);
@@ -904,7 +939,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 *att_map)
 {
 	TupleDesc	desc;
 	int			i;
@@ -914,20 +949,35 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 
 	desc = RelationGetDescr(rel);
 
-	/* send number of live attributes */
-	for (i = 0; i < desc->natts; i++)
-	{
-		if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
-			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 number of live attributes */
+	for (i = 0; i < desc->natts; i++)
+	{
+		Form_pg_attribute att = TupleDescAttr(desc, i);
+
+		if (att->attisdropped || att->attgenerated)
+			continue;
+		/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
+		if (replidentfull ||
+			bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						  idattrs))
+		{
+			nliveatts++;
+			continue;
+		}
+		/* Skip sending if not a part of column filter */
+		if (att_map != NULL &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   att_map))
+			continue;
+		nliveatts++;
+	}
+	pq_sendint16(out, nliveatts);
+
 	/* send the attributes */
 	for (i = 0; i < desc->natts; i++)
 	{
@@ -937,6 +987,17 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
+		/*
+		 * Exclude filtered columns, but REPLICA IDENTITY columns can't be
+		 * excluded
+		 */
+		if (att_map != NULL &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   att_map) &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   idattrs)
+			&& !replidentfull)
+			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 f07983a43c..15902faf56 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -111,6 +111,7 @@
 #include "replication/origin.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -695,19 +696,25 @@ fetch_remote_table_info(char *nspname, char *relname,
 						LogicalRepRelation *lrel)
 {
 	WalRcvExecResult *res;
+	WalRcvExecResult *res_pub;
 	StringInfoData cmd;
 	TupleTableSlot *slot;
-	Oid			tableRow[] = {OIDOID, CHAROID, CHAROID};
+	TupleTableSlot *slot_pub;
+	Oid			tableRow[] = {OIDOID, CHAROID, CHAROID, BOOLOID};
 	Oid			attrRow[] = {TEXTOID, OIDOID, BOOLOID};
+	Oid			pubRow[] = {TEXTARRAYOID};
 	bool		isnull;
 	int			natt;
+	List	   *pub_columns = NIL;
+	ListCell   *lc;
+	bool		am_partition = false;
 
 	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)"
@@ -737,6 +744,7 @@ fetch_remote_table_info(char *nspname, char *relname,
 	Assert(!isnull);
 	lrel->relkind = DatumGetChar(slot_getattr(slot, 3, &isnull));
 	Assert(!isnull);
+	am_partition = DatumGetChar(slot_getattr(slot, 4, &isnull));
 
 	ExecDropSingleTupleTableSlot(slot);
 	walrcv_clear_result(res);
@@ -774,11 +782,101 @@ fetch_remote_table_info(char *nspname, char *relname,
 
 	natt = 0;
 	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+
+	/*
+	 * Now, fetch the values of publications' column filters.
+	 *
+	 * For a partition, use pg_inherit to find the parent, as the
+	 * pg_publication_rel contains only the topmost parent table entry in case
+	 * the table is partitioned.  Run a recursive query to iterate through all
+	 * the parents of the partition and retreive the record for the parent
+	 * that exists in pg_publication_rel.
+	 */
+	resetStringInfo(&cmd);
+	appendStringInfoString(&cmd,
+						   "SELECT CASE WHEN prattrs IS NOT NULL THEN\n"
+						   "           ARRAY(SELECT attname\n"
+						   "                   FROM pg_catalog.generate_series(0, pg_catalog.array_upper(prattrs::int[], 1)) s,\n"
+						   "                        pg_catalog.pg_attribute\n"
+						   "                  WHERE attrelid = prrelid AND attnum = prattrs[s])\n"
+						   "           ELSE NULL END AS columns\n"
+						   "FROM pg_catalog.pg_publication_rel\n");
+	if (!am_partition)
+		appendStringInfo(&cmd, "WHERE prrelid = %u", lrel->remoteid);
+	else
+		appendStringInfo(&cmd,
+						 "WHERE prrelid IN (SELECT relid \n"
+						 "FROM pg_catalog.pg_partition_tree(pg_catalog.pg_partition_root(%u)))",
+						 lrel->remoteid);
+
+	res_pub = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
+						  lengthof(pubRow), pubRow);
+
+	if (res_pub->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				(errcode(ERRCODE_CONNECTION_FAILURE),
+				 errmsg("could not fetch published columns info for table \"%s.%s\" from publisher: %s",
+						nspname, relname, res_pub->err)));
+	slot_pub = MakeSingleTupleTableSlot(res_pub->tupledesc, &TTSOpsMinimalTuple);
+
+	while (tuplestore_gettupleslot(res_pub->tuplestore, true, false, slot_pub))
+	{
+		Datum		adatum;
+		Datum	   *elems;
+		bool	   *nulls;
+		int			nelems;
+
+		adatum = slot_getattr(slot_pub, 1, &isnull);
+		if (isnull)			/* shouldn't happen */
+			elog(ERROR, "unexpected null value in publication column filter");
+		deconstruct_array(DatumGetArrayTypeP(adatum),
+						  TEXTOID, -1, false, TYPALIGN_INT,
+						  &elems, &nulls, &nelems);
+		for (int i = 0; i < nelems; i++)
+		{
+			if (nulls[i])	/* shouldn't happen */
+				elog(ERROR, "unexpected null value in publication column filter");
+			pub_columns = lappend(pub_columns, TextDatumGetCString(elems[i]));
+		}
+		ExecClearTuple(slot_pub);
+	}
+	ExecDropSingleTupleTableSlot(slot_pub);
+	walrcv_clear_result(res_pub);
+
+	/*
+	 * Store the column names only if they are contained in column filter
+	 * LogicalRepRelation will only contain attributes corresponding to those
+	 * specficied in column filters.
+	 */
 	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
 	{
-		lrel->attnames[natt] =
-			TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		char	   *rel_colname;
+		bool		found = false;
+
+		rel_colname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
 		Assert(!isnull);
+		if (pub_columns != NIL)
+		{
+			foreach(lc, pub_columns)
+			{
+				char	   *pub_colname = lfirst(lc);
+
+				if (!strcmp(pub_colname, rel_colname))
+				{
+					found = true;
+					lrel->attnames[natt] = rel_colname;
+					break;
+				}
+			}
+		}
+		else
+		{
+			found = true;
+			lrel->attnames[natt] = rel_colname;
+		}
+		if (!found)
+			continue;
+
 		lrel->atttyps[natt] = DatumGetObjectId(slot_getattr(slot, 2, &isnull));
 		Assert(!isnull);
 		if (DatumGetBool(slot_getattr(slot, 3, &isnull)))
@@ -829,8 +927,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 6f6a203dea..f9f9ecd0c0 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 *att_map);
 static void send_repl_origin(LogicalDecodingContext *ctx,
 							 RepOriginId origin_id, XLogRecPtr origin_lsn,
 							 bool send_origin);
@@ -130,6 +134,7 @@ typedef struct RelationSyncEntry
 	 * having identical TupleDesc.
 	 */
 	TupleConversionMap *map;
+	Bitmapset  *att_map;
 } RelationSyncEntry;
 
 /* Map used to remember which relation schemas we sent. */
@@ -570,11 +575,11 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 		}
 
 		MemoryContextSwitchTo(oldctx);
-		send_relation_and_attrs(ancestor, xid, ctx);
+		send_relation_and_attrs(ancestor, xid, ctx, relentry->att_map);
 		RelationClose(ancestor);
 	}
 
-	send_relation_and_attrs(relation, xid, ctx);
+	send_relation_and_attrs(relation, xid, ctx, relentry->att_map);
 
 	if (in_streaming)
 		set_schema_sent_in_streamed_txn(relentry, topxid);
@@ -587,7 +592,8 @@ maybe_send_schema(LogicalDecodingContext *ctx,
  */
 static void
 send_relation_and_attrs(Relation relation, TransactionId xid,
-						LogicalDecodingContext *ctx)
+						LogicalDecodingContext *ctx,
+						Bitmapset *att_map)
 {
 	TupleDesc	desc = RelationGetDescr(relation);
 	int			i;
@@ -610,13 +616,25 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		if (att->atttypid < FirstGenbkiObjectId)
 			continue;
 
+		/*
+		 * Do not send type information if attribute is not present in column
+		 * filter. XXX Allow sending type information for REPLICA IDENTITY
+		 * COLUMNS with user created type. even when they are not mentioned in
+		 * column filters.
+		 *
+		 * FIXME -- this code seems not verified by tests.
+		 */
+		if (att_map != NULL &&
+			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						   att_map))
+			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, att_map);
 	OutputPluginWrite(ctx, false);
 }
 
@@ -693,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->att_map);
 				OutputPluginWrite(ctx, true);
 				break;
 			}
@@ -722,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->att_map);
 				OutputPluginWrite(ctx, true);
 				break;
 			}
@@ -1122,6 +1140,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 	bool		am_partition = get_rel_relispartition(relid);
 	char		relkind = get_rel_relkind(relid);
 	bool		found;
+	Oid			ancestor_id;
 	MemoryContext oldctx;
 
 	Assert(RelationSyncCache != NULL);
@@ -1142,6 +1161,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->att_map = NULL;
 		entry->map = NULL;		/* will be set by maybe_send_schema() if
 								 * needed */
 	}
@@ -1182,6 +1202,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 		{
 			Publication *pub = lfirst(lc);
 			bool		publish = false;
+			bool		ancestor_published = false;
 
 			if (pub->alltables)
 			{
@@ -1192,8 +1213,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
@@ -1219,6 +1238,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 											pub->oid))
 						{
 							ancestor_published = true;
+							ancestor_id = ancestor;
 							if (pub->pubviaroot)
 								publish_as_relid = ancestor;
 						}
@@ -1239,15 +1259,47 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 			if (publish &&
 				(relkind != RELKIND_PARTITIONED_TABLE || pub->pubviaroot))
 			{
+				Oid			relid;
+				HeapTuple	pub_rel_tuple;
+
+				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)
+					{
+						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->att_map = bms_add_member(entry->att_map,
+															elems[i] - FirstLowInvalidHeapAttributeNumber);
+						MemoryContextSwitchTo(oldctx);
+					}
+					ReleaseSysCache(pub_rel_tuple);
+				}
 				entry->pubactions.pubinsert |= pub->pubactions.pubinsert;
 				entry->pubactions.pubupdate |= pub->pubactions.pubupdate;
 				entry->pubactions.pubdelete |= pub->pubactions.pubdelete;
 				entry->pubactions.pubtruncate |= pub->pubactions.pubtruncate;
 			}
-
-			if (entry->pubactions.pubinsert && entry->pubactions.pubupdate &&
-				entry->pubactions.pubdelete && entry->pubactions.pubtruncate)
-				break;
 		}
 
 		list_free(pubids);
@@ -1343,6 +1395,8 @@ rel_sync_cache_relation_cb(Datum arg, Oid relid)
 		entry->schema_sent = false;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
+		bms_free(entry->att_map);
+		entry->att_map = NULL;
 		if (entry->map)
 		{
 			/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 10a86f9810..0c438481dc 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4265,6 +4265,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_oid;
 	int			i_prpubid;
 	int			i_prrelid;
+	int			i_prattrs;
 	int			i,
 				j,
 				ntups;
@@ -4276,8 +4277,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);
@@ -4286,6 +4292,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));
@@ -4327,6 +4334,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);
 
@@ -4391,10 +4420,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 6dccb4be4e..50a5b885f6 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -633,6 +633,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 72d8547628..46fa616406 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6302,7 +6302,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;
@@ -6319,10 +6319,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);
@@ -6450,8 +6454,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::int[], 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/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2f412ca3db..84ee807e0b 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1648,6 +1648,8 @@ psql_completion(const char *text, int start, int end)
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3eca295ff4..76d421e09e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -214,6 +214,9 @@ extern long changeDependenciesOf(Oid classId, Oid oldObjectId,
 extern long changeDependenciesOn(Oid refClassId, Oid oldRefObjectId,
 								 Oid newRefObjectId);

diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 902f2f2f0d..f5ae2065e9 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);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index b5d5504cbb..7ad285faae 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
+	int2vector	prattrs;		/* Variable length field starts here */
+#endif
 } FormData_pg_publication_rel;
 
 /* ----------------
 
 extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4c5a8a39bf..02b547d044 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3642,6 +3642,7 @@ typedef struct PublicationTable
 {
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to be published */
+	List	   *columns;		/* List of columns in a publication table */
 } PublicationTable;
 
 /*
diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h
index 83741dcf42..709b4be916 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 *att_map);
 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 *att_map);
 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 *att_map);
 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 5ac2d666a2..84afe0ebef 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,7 +165,35 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
@@ -669,6 +697,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 56dd358554..200158ba69 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,7 +89,20 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables

 CREATE TABLE testpub_tbl3 (a int);
@@ -362,6 +375,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/021_column_filter.pl b/src/test/subscription/t/021_column_filter.pl
new file mode 100644
index 0000000000..354e6ac363
--- /dev/null
+++ b/src/test/subscription/t/021_column_filter.pl
@@ -0,0 +1,162 @@
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# Test TRUNCATE
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 10;
+
+# 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)");
+# 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 column filtering
+$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 c is not replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab3");
+is($result, qq(1|3), 'insert on column 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 c is not replicated');
+
+#Test alter publication with column filtering
+$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->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab2");
+is($result, qq(1|abc), 'insert on column c is not replicated');
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET c = 5 where a = 1");
+is($result, qq(1|abc), 'update on column c is not replicated');
+
+# Test behavior when a column is dropped
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE test_part DROP COLUMN b");
+$result = $node_publisher->safe_psql('postgres',
+	"select prrelid::regclass, prattrs from pg_publication_rel pb;");
+is($result,
+	q(tab1|1 2
+tab3|1 3
+tab2|1 2
+test_part|1), 'column test_part.b removed');
+
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO test_part VALUES (3, '2021-12-13 12:13:14')");
+$node_publisher->wait_for_catchup('sub1');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM test_part WHERE a = 3");
+is($result, "3|", 'only column a is replicated');
+
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab4 (a int PRIMARY KEY, b int, c int, d int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab4 (a int PRIMARY KEY, b int, d int)");
+$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub2 FOR TABLE tab4 (a, b)");
+$node_publisher->safe_psql('postgres', "CREATE PUBLICATION pub3 FOR TABLE tab4 (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 tab4 VALUES (1, 11, 111, 1111)");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2, 22, 222, 2222)");
+$node_publisher->wait_for_catchup('sub2');
+is($node_subscriber->safe_psql('postgres',"SELECT * FROM tab4;"),
+   qq(1|11|1111
+2|22|2222),
+   'overlapping publications with overlapping column lists');

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 13 Dec 2021 18:19:54 -0300

> Hmm, I messed up the patch file I sent.  Here's the complete patch.

Actually, this requires even a bit more mess than this to be really
complete if we want to be strict about it.  The reason is that, with the
patch I just posted, we're creating a new type of representable object
that will need to have some way of making it through pg_identify_object,
pg_get_object_address, pg_identify_object_as_address.  This is only
visible as one tries to patch object_address.sql (auditability of DDL
operations being the goal).

I think this means we need a new OBJECT_PUBLICATION_REL_COLUMN value in
the ObjectType (paralelling OBJECT_COLUMN), and no new ObjectClass
value.  Looking now to confirm.

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
"El que vive para el futuro es un iluso, y el que vive para el pasado,
un imbécil" (Luis Adler, "Los tripulantes de la noche")

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 14 Dec 2021 13:43:48 -0300

> value.  Looking now to confirm.

After working on this a little bit more, I realized that this is a bad
idea overall.  It causes lots of complications and it's just not worth
it.  So I'm back at my original thought that we need to throw an ERROR
at ALTER TABLE .. DROP COLUMN time if the column is part of a
replication column filter, and suggest the user to remove the column
from the filter first and reattempt the DROP COLUMN.

This means that we need to support changing the column list of a table
in a publication.  I'm looking at implementing some form of ALTER
PUBLICATION for that.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Find a bug in a program, and fix it, and the program will work today.
Show the program how to find and fix a bug, and the program
will work forever" (Oliver Silfridge)

---

From: Tomas Vondra <[email-masked]>
Date: Tue, 14 Dec 2021 20:28:53 +0100

> in a publication.  I'm looking at implementing some form of ALTER

Yeah. I think it's not clear if this should behave more like an index or 
a view. When an indexed column gets dropped we simply drop the index. 
But if you drop a column referenced by a view, we fail with an error. I 
think we should handle this more like a view, because publications are 
externally visible objects too (while indexes are pretty much just an 
implementation detail).

But why would it be easier not to add new object type? We still need to 
check there is no publication referencing the column - either you do 
that automatically through a dependency, or you do that by custom code. 
Using a dependency seems better to me, but I don't know what are the 
complications you mentioned.

regards

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 14 Dec 2021 16:35:23 -0300

On 2021-Dec-14, Tomas Vondra wrote:

> Yeah. I think it's not clear if this should behave more like an index or a
> view. When an indexed column gets dropped we simply drop the index. But if
> you drop a column referenced by a view, we fail with an error. I think we
> should handle this more like a view, because publications are externally
> visible objects too (while indexes are pretty much just an implementation
> detail).

I agree -- I think it's more like a view than like an index.  (The
original proposal was that if you dropped a column that was part of the
column list of a relation in a publication, the entire relation is
dropped from the view, but that doesn't seem very friendly behavior --
you break the replication stream immediately if you do that, and the
only way to fix it is to send a fresh copy of the remaining subset of
columns.)

> check there is no publication referencing the column - either you do that
> automatically through a dependency, or you do that by custom code. Using a
> dependency seems better to me, but I don't know what are the complications
> you mentioned.

The problem is that we need a way to represent the object "column of a
table in a publication".  I found myself adding a lot of additional code
to support OBJECT_PUBLICATION_REL_COLUMN and that seemed like too much.

---

From: Tomas Vondra <[email-masked]>
Date: Tue, 14 Dec 2021 22:13:49 +0100

> only way to fix it is to send a fresh copy of the remaining subset of

Right, that's my reasoning too.

> to support OBJECT_PUBLICATION_REL_COLUMN and that seemed like too much.

My experience with dependencies is pretty limited, but can't we simply 
make a dependency between the whole publication and the column?

regards

---

From: Tomas Vondra <[email-masked]>
Date: Tue, 14 Dec 2021 22:28:11 +0100

Hi,

I went through the v9 patch, and I have a couple comments / questions. 
Apologies if some of this was already discussed earlier, it's hard to 
cross-check in such a long thread. Most of the comments are in 0002 to 
make it easier to locate, and it also makes proposed code changes 
clearer I think.

1) check_publication_add_relation - the "else" branch is not really 
needed, because the "if (replidentfull)" always errors-out

2) publication_add_relation has a FIXME about handling cases with 
different column list

So what's the right behavior for ADD TABLE with different column list? 
I'd say we should allow that, and that it should be mostly the same 
thing as adding/removing columns to the list incrementally, i.e. we 
should replace the column lists. We could also prohibit such changes, 
but that seems like a really annoying limitation, forcing people to 
remove/add the relation.

I added some comments to the attmap translation block, and replaced <0 
check with AttrNumberIsForUserDefinedAttr.

But I wonder if we could get rid of the offset, considering we're 
dealing with just user-defined attributes. That'd make the code clearer, 
but it would break if we're comparing it to other bitmaps with offsets. 
But I don't think we do.

3) I doubt "att_map" is the right name, though. AFAICS it's just a list 
of columns for the relation, not a map, right? So maybe attr_list?

4) AlterPublication talks about "publication status" for a column, but 
do we actually track that? Or what does that mean?

5) PublicationDropTables does a check

     if (pubrel->columns)
         ereport(ERROR,
                 errcode(ERRCODE_SYNTAX_ERROR),

Shouldn't this be prevented by the grammar, really? Also, it should be 
in regression tests.

6) Another thing that should be in the test is partitioned table with 
attribute mapping and column list, to see how map and attr_map interact.

7) There's a couple places doing this

     if (att_map != NULL &&
                        att_map) &&
                        idattrs) &&
         !replidentfull)

which is really hard to understand (even if we get rid of the offset), 
so maybe let's move that to a function with sensible name. Also, some 
places don't check indattrs - seems a bit suspicious.

regards

From fb5ce02d36b46f92ab01c9a823cc4e315cfcb73c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 14 Dec 2021 20:53:28 +0100
Subject: [PATCH 1/2] v9

---
 doc/src/sgml/ref/alter_publication.sgml      |   4 +-
 doc/src/sgml/ref/create_publication.sgml     |  11 +-
 src/backend/catalog/dependency.c             |   8 +-
 src/backend/catalog/objectaddress.c          |   8 +
 src/backend/catalog/pg_depend.c              |  50 ++++++
 src/backend/catalog/pg_publication.c         | 106 +++++++++++-
 src/backend/commands/publicationcmds.c       |  94 ++++++++++-
 src/backend/commands/tablecmds.c             |  10 +-
 src/backend/nodes/copyfuncs.c                |   1 +
 src/backend/nodes/equalfuncs.c               |   1 +
 src/backend/parser/gram.y                    |  36 ++++-
 src/backend/replication/logical/proto.c      |  97 ++++++++---
 src/backend/replication/logical/tablesync.c  | 117 +++++++++++++-
 src/backend/replication/pgoutput/pgoutput.c  |  80 +++++++--
 src/bin/pg_dump/pg_dump.c                    |  41 ++++-
 src/bin/pg_dump/pg_dump.h                    |   1 +
 src/bin/psql/describe.c                      |  26 ++-
 src/bin/psql/tab-complete.c                  |   2 +
 src/include/catalog/dependency.h             |   3 +
 src/include/catalog/pg_publication.h         |   1 +
 src/include/catalog/pg_publication_rel.h     |   3 +
 src/include/commands/publicationcmds.h       |   2 +-
 src/include/nodes/parsenodes.h               |   1 +
 src/include/replication/logicalproto.h       |   6 +-
 src/test/regress/expected/publication.out    |  39 ++++-
 src/test/regress/sql/publication.sql         |  19 ++-
 src/test/subscription/t/021_column_filter.pl | 162 +++++++++++++++++++
 27 files changed, 857 insertions(+), 72 deletions(-)
 create mode 100644 src/test/subscription/t/021_column_filter.pl

 

 
+     <para>

 		case OCLASS_PUBLICATION:

 				if (objargs)
+				{
 
 				ReleaseSysCache(tup);
 				break;
 
+/*
 
 /*
 
 /*
 
 /*

 

 
 	/* Form a tuple. */

 
+	/*

 
 /*

 
 	ReleaseSysCache(tup);

 

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

 	return newnode;
 }
 
 	return true;
 }

 

 /*

 
 /*

 /*

 
 /*

 
+		/*

+	/* fetch bitmap of REPLICATION IDENTITY attributes */
 	/* send number of live attributes */
 
-	/* 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++)
 	{
@@ -937,6 +987,17 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
 		if (att->attisdropped || att->attgenerated)
 			continue;
 
+		/*
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;

 
 	natt = 0;

 

 	if (in_streaming)
 
+		/*

 

 
 			if (pub->alltables)
 			{
 
 			if (!publish)
 			{
 
 		list_free(pubids);

 	ntups = PQntuples(res);

 

 
 	/*
 
 /*

 

 
 /* ----------------

 /*

 

2.31.1

From a1cb64a17b35dd75daf22a9f59da176ed109612a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Tue, 14 Dec 2021 22:11:10 +0100
Subject: [PATCH 2/2] review

---
 src/backend/catalog/pg_publication.c        | 60 +++++++++++++++------
 src/backend/commands/publicationcmds.c      | 12 +++++
 src/backend/commands/tablecmds.c            |  2 +-
 src/backend/replication/logical/proto.c     |  8 +++
 src/backend/replication/pgoutput/pgoutput.c |  8 +++
 5 files changed, 72 insertions(+), 18 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ae58adc8e5..7a478b7072 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -99,28 +99,27 @@ check_publication_add_relation(Relation targetrel, Bitmapset *columns)
 	 */
 	if (columns != NULL)
 	{
+		Bitmapset  *idattrs;
+
 		if (replidentfull)
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("invalid column list for publishing relation \"%s\"",
 						   RelationGetRelationName(targetrel)),
 					errdetail("Cannot have column filter on relations with REPLICA IDENTITY FULL."));
-		else
-		{
-			Bitmapset  *idattrs;
-
-			idattrs = RelationGetIndexAttrBitmap(targetrel,
-												 INDEX_ATTR_BITMAP_IDENTITY_KEY);
-			if (!bms_is_subset(idattrs, 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."));
-
-			if (idattrs)
-				pfree(idattrs);
-		}
+
+		/* XXX The else was unnecessary, because the "if" always errors-out */
+		idattrs = RelationGetIndexAttrBitmap(targetrel,
+											 INDEX_ATTR_BITMAP_IDENTITY_KEY);
+		if (!bms_is_subset(idattrs, 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."));
+
+		if (idattrs)
+			pfree(idattrs);
 	}
 }
 
@@ -352,6 +351,15 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 
 		/* FIXME need to handle the case of different column list */
 
+		/*
+		 * XXX So what's the right behavior for ADD TABLE with different column
+		 * list? I'd say we should allow that, and that it should be mostly the
+		 * same thing as adding/removing columns to the list incrementally, i.e.
+		 * we should replace the column lists. We could also prohibit, but that
+		 * seems like a really annoying limitation, forcing people to remove/add
+		 * the relation.
+		 */
+
 		if (if_not_exists)
 			return InvalidObjectAddress;
 
@@ -361,6 +369,10 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 						RelationGetRelationName(targetrel->relation), pub->name)));
 	}
 
+	/*
+	 * Translate list of columns to attnums. We prohibit system attributes and
+	 * make sure there are no duplicate columns.
+	 */
 	attarray = palloc(sizeof(AttrNumber) * list_length(targetrel->columns));
 	foreach(lc, targetrel->columns)
 	{
@@ -372,12 +384,23 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 					errcode(ERRCODE_UNDEFINED_COLUMN),
 					errmsg("column \"%s\" of relation \"%s\" does not exist",
 						   colname, RelationGetRelationName(targetrel->relation)));
-		if (attnum < 0)
+
+		if (!AttrNumberIsForUserDefinedAttr(attnum))
 			ereport(ERROR,
 					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
 					errmsg("cannot reference system column \"%s\" in publication column list",
 						   colname));
 
+		/*
+		 * XXX The offset seems kinda pointless, because at this point we're not
+		 * dealing with system attributes, so all attnums are > 0. Are we comparing
+		 * it to arbitrary attnums later? This would simplify adding the deps later
+		 * because we're offsetting it back.
+		 *
+		 * XXX I wouldn't say "twice" though. It can be specified multiple times,
+		 * it's just that we discover it during the second occurrence. I'd say
+		 * "duplicate column name in publication column list" instead.
+		 */
 		if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, attmap))
 			ereport(ERROR,
 					errcode(ERRCODE_DUPLICATE_OBJECT),
@@ -417,6 +440,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	CatalogTupleInsert(rel, tup);
 	heap_freetuple(tup);
 
+	/* XXX not sure if worth an explicit free, but if we free the tuple ... */
+	pfree(attarray);
+
 	ObjectAddressSet(myself, PublicationRelRelationId, prrelid);
 
 	/* Add dependency on the publication */
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a070914bdd..38efc5ad59 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -759,6 +759,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 /*
  * Remove relation from publication by mapping OID, or publication status
  * of one column of that relation in the publication if an attnum is given.
+ *
+ * XXX Whats "publication status"?
  */
 void
 RemovePublicationRelById(Oid proid, int32 attnum)
@@ -825,6 +827,9 @@ RemovePublicationRelById(Oid proid, int32 attnum)
 								 &isnull);
 		if (isnull)			/* shouldn't happen */
 			elog(ERROR, "can't drop column from publication without a column list");
+
+		/* XXX We're reading the array in multiple places, I suggest to move it
+		 * to a separate function, maybe? */
 		arr = DatumGetArrayTypeP(adatum);
 		nelems = ARR_DIMS(arr)[0];
 		elems = (int16 *) ARR_DATA_PTR(arr);
@@ -833,6 +838,7 @@ RemovePublicationRelById(Oid proid, int32 attnum)
 		newelems = palloc(sizeof(int16) * nelems - 1);
 		for (i = 0, j = 0; i < nelems - 1; i++)
 		{
+			/* XXX Can it happen that we never find a match? Seems like an error. */
 			if (elems[i] == attnum)
 				continue;
 			newelems[j++] = elems[i];
@@ -842,6 +848,10 @@ RemovePublicationRelById(Oid proid, int32 attnum)
 		 * If this is the last column used in the publication, disallow the
 		 * command. We could alternatively just drop the relation from the
 		 * publication.
+		 *
+		 * XXX Alternatively we could switch to "replicate all column", but
+		 * that seems counterintuitive. However, is there a way to switch
+		 * between these two modes, somehow?
 		 */
 		if (j == 0)
 		{
@@ -1154,6 +1164,8 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 		Relation	rel = pubrel->relation;
 		Oid			relid = RelationGetRelid(rel);
 
+		/* XXX Shouldn't this be prevented by the grammar, ideally? Can it actually
+		 * happen? It does not seem to be tested in the regression tests. */
 		if (pubrel->columns)
 			ereport(ERROR,
 					errcode(ERRCODE_SYNTAX_ERROR),
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7207dcf9c0..f7c21158c0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8422,7 +8422,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 	ReleaseSysCache(tuple);
 
 	/*
-	 * If the column is part of a replication column list, arrange to get that
+	 * If the column is part of a publication column list, arrange to get that
 	 * removed too.
 	 */
 	findAndAddAddresses(addrs, PublicationRelRelationId,
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index 15d8192238..885acd6c9e 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -777,6 +777,13 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple, bool binar
 		 * Do not increment count of attributes if not a part of column
 		 * filters except for replica identity columns or if replica identity
 		 * is full.
+		 *
+		 * XXX This exact check is done in multiple places, so maybe let's move it
+		 * to a separate function and call it. Also, att_map is for user attrs only
+		 * so maybe we can get rid of the offsets?
+		 *
+		 * XXX Why do we need to check both att_map and idattrs? Aren't we enforcing
+		 * that indattrs is always a subset of att_map?
 		 */
 		if (att_map != NULL &&
 			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
@@ -970,6 +977,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel, Bitmapset *att_map)
 			continue;
 		}
 		/* Skip sending if not a part of column filter */
+		/* XXX How come this is not checking idattrs and replindentfull? */
 		if (att_map != NULL &&
 			!bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
 						   att_map))
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index f9f9ecd0c0..551c4d5315 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -134,6 +134,14 @@ typedef struct RelationSyncEntry
 	 * having identical TupleDesc.
 	 */
 	TupleConversionMap *map;
+
+	/* XXX Needs a comment explaining what att_map does, how it's different
+	 * from map. The naming seems really confusing and it's a bit unclear how
+	 * these two bits interact (i.e. partitioning + column list).
+	 *
+	 * XXX In fact, is it really mapping anything? It seems more like a simple
+	 * list of attnums, translated from list of names.
+	 */
 	Bitmapset  *att_map;
 } RelationSyncEntry;
 
-- 
2.31.1

---

From: "[email-masked]" <[email-masked]>
Date: Thu, 16 Dec 2021 09:28:28 +0000

Hi,

I have a minor question about the replica identity check of this patch.

The patch ensures all columns of RT are in column list when CREATE/ALTER
publication, but it seems doesn't prevent user from changing the replica
identity or dropping the index used in replica identity. Do we also need to
check those cases ?

Best regards,
Hou zj

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 16 Dec 2021 14:54:59 -0300

> identity or dropping the index used in replica identity. Do we also need to

Yes, we do.  As it happens, I spent a couple of hours yesterday writing
code for that, at least partially.  I haven't yet checked what happens
with cases like REPLICA NOTHING, or REPLICA INDEX <xyz> and then
dropping that index.

My initial ideas were a bit wrong BTW: I thought we should check the
combination of column lists in all publications (a bitwise-OR of column
bitmaps, so to speak).  But conceptually that's wrong: we need to check
the column list of each publication individually instead.  Otherwise, if
you wanted to hide a column from some publication but that column was
part of the replica identity, there'd be no way to identify the tuple in
the replica.  (Or, if the pgouput code disobeys the column list and
sends the replica identity even if it's not in the column list, then
you'd be potentially publishing data that you wanted to hide.)

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 16 Dec 2021 17:10:58 -0300

On 2021-Dec-14, Tomas Vondra wrote:

> which is really hard to understand (even if we get rid of the offset), so
> maybe let's move that to a function with sensible name. Also, some places
> don't check indattrs - seems a bit suspicious.

It is indeed pretty hard to read ... but I think this is completely
unnecessary.  Any column that is part of the identity should have been
included in the column filter, so there is no need to check for the
identity attributes separately.  Testing just for the columns in the
filter ought to be sufficient; and the cases "if att_map NULL" and "is
replica identity FULL" are also equivalent, because in the case of FULL,
you're disallowed from setting a column list.  So this whole thing can
be reduced to just this:

if (att_map != NULL && !bms_is_member(att->attnum, att_map))
       continue;	/* that is, don't send this attribute */

so I don't think this merits a separate function.

[ says he, after already trying to write said function ]

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Before you were born your parents weren't as boring as they are now. They
got that way paying your bills, cleaning up your room and listening to you
tell them how idealistic you are."  -- Charles J. Sykes' advice to teenagers

---

From: Amit Kapila <[email-masked]>
Date: Fri, 17 Dec 2021 10:17:33 +0530

On Wed, Dec 15, 2021 at 1:05 AM Alvaro Herrera <[email-masked]> wrote:
> dropped from the view,

I think in the above sentence, you mean to say "dropped from the
publication". So, IIUC, you are proposing that if one drops a column
that was part of the column list of a relation in a publication, an
error will be raised. Also, if the user specifies CASCADE in Alter
Table ... Drop Column, then we drop the relation from publication. Is
that right? BTW, this is somewhat on the lines of what row_filter
patch is also doing where if the user drops the column that was part
of row_filter for a relation in publication, we give an error and if
the user tries to drop the column with CASCADE then the relation is
removed from the publication.

-- 
With Regards,
Amit Kapila.

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 17 Dec 2021 08:03:33 +0100

> removed from the publication.

That looks correct.  Consider how triggers behave: Dropping a column 
that a trigger uses (either in UPDATE OF or a WHEN condition) errors 
with RESTRICT and drops the trigger with CASCADE.

---

From: "[email-masked]" <[email-masked]>
Date: Fri, 17 Dec 2021 09:46:14 +0000

On Friday, December 17, 2021 1:55 AM Alvaro Herrera <[email-masked]> wrote:
> > The patch ensures all columns of RT are in column list when
> > CREATE/ALTER publication, but it seems doesn't prevent user from
> > changing the replica identity or dropping the index used in replica
> > identity. Do we also need to check those cases ?
> 
> Yes, we do.  As it happens, I spent a couple of hours yesterday writing code for
> that, at least partially.  I haven't yet checked what happens with cases like
> REPLICA NOTHING, or REPLICA INDEX <xyz> and then dropping that index.
> 
> combination of column lists in all publications (a bitwise-OR of column bitmaps,
> so to speak).  But conceptually that's wrong: we need to check the column list
> of each publication individually instead.  Otherwise, if you wanted to hide a
> column from some publication but that column was part of the replica identity,
> there'd be no way to identify the tuple in the replica.  (Or, if the pgouput code
> disobeys the column list and sends the replica identity even if it's not in the
> column list, then you'd be potentially publishing data that you wanted to hide.)

Thanks for the explanation.

Apart from ALTER REPLICA IDENTITY and DROP INDEX, I think there could be
some other cases we need to handle for the replica identity check:

1)
When adding a partitioned table with column list to the publication, I think we
need to check the RI of all its leaf partition. Because the RI on the partition
is the one actually takes effect.

2)
ALTER TABLE ADD PRIMARY KEY;
ALTER TABLE DROP CONSTRAINT "PRIMAEY KEY";

If the replica identity is default, it will use the primary key. we might also
need to prevent user from adding or removing primary key in this case.

Based on the above cases, the RI check seems could bring considerable amount of
code. So, how about we follow what we already did in CheckCmdReplicaIdentity(),
we can put the check for RI in that function, so that we can cover all the
cases and reduce the code change. And if we are worried about the cost of do
the check for UPDATE and DELETE every time, we can also save the result in the
relcache. It's safe because every operation change the RI will invalidate the
relcache. We are using this approach in row filter patch to make sure all
columns in row filter expression are part of RI.

Best regards,
Hou zj

---

From: Rahila Syed <[email-masked]>
Date: Fri, 17 Dec 2021 17:39:37 +0530

Hi,

Thank you for updating the patch. The regression tests and tap tests pass
with v9 patch.

> in a publication.  I'm looking at implementing some form of ALTER
I think right now the patch contains support only for ALTER PUBLICATION..
ADD TABLE with column filters.
In order to achieve changing the column lists of a published table, I think
we can extend the
ALTER TABLE ..SET TABLE syntax to support specification of column list.

So this whole thing can

>        continue;        /* that is, don't send this attribute */

I agree the condition can be shortened now. The long if condition was
included because initially the feature
allowed specifying filters without replica identity columns(sent those
columns internally without user
having to specify).

 900 +        * the table is partitioned.  Run a recursive query to iterate
> through all
>  901 +        * the parents of the partition and retreive the record for
> the parent
>  902 +        * that exists in pg_publication_rel.
>  903 +        */

The above comment in fetch_remote_table_info() can be changed as the
recursive query
is no longer used.

Thank you,
Rahila Syed

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 17 Dec 2021 12:58:50 -0300

On 2021-Dec-17, Rahila Syed wrote:

> > This means that we need to support changing the column list of a
> > table in a publication.  I'm looking at implementing some form of
> > ALTER PUBLICATION for that.
>
> I think right now the patch contains support only for ALTER
> PUBLICATION..  ADD TABLE with column filters.  In order to achieve
> changing the column lists of a published table, I think we can extend
> the ALTER TABLE ..SET TABLE syntax to support specification of column
> list.

Yeah, that's what I was thinking too.

> > So this whole thing can be reduced to just this:
> 
> included because initially the feature allowed specifying filters
> without replica identity columns(sent those columns internally without
> user having to specify).

Ah, true, I had forgotten that.  Thanks.

> >  900 +        * the table is partitioned.  Run a recursive query to iterate through all
> >  901 +        * the parents of the partition and retreive the record for the parent
> recursive query is no longer used.

Oh, of course.

I'll finish some loose ends and submit a v10, but it's still not final.

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/
"Right now the sectors on the hard disk run clockwise, but I heard a rumor that
you can squeeze 0.2% more throughput by running them counterclockwise.
It's worth the effort. Recommended."  (Gerry Pourwelle)

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 17 Dec 2021 18:07:18 -0300

So I've been thinking about this as a "security" item (you can see my
comments to that effect sprinkled all over this thread), in the sense
that if a publication "hides" some column, then the replica just won't
get access to it.  But in reality that's mistaken: the filtering that
this patch implements is done based on the queries that *the replica*
executes at its own volition; if the replica decides to ignore the list
of columns, it'll be able to get all columns.  All it takes is an
uncooperative replica in order for the lot of data to be exposed anyway.

If the server has a *separate* security mechanism to hide the columns
(per-column privs), it is that feature that will protect the data, not
the logical-replication-feature to filter out columns.

This led me to realize that the replica-side code in tablesync.c is
totally oblivious to what's the publication through which a table is
being received from in the replica.  So we're not aware of a replica
being exposed only a subset of columns through some specific
publication; and a lot more hacking is needed than this patch does, in
order to be aware of which publications are being used.

I'm going to have a deeper look at this whole thing.

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 17 Dec 2021 22:57:56 +0100

> uncooperative replica in order for the lot of data to be exposed anyway.

Interesting, I haven't really looked at this as a security feature. And 
in my experience if something is not carefully designed to be secure 
from the get go, it's really hard to add that bit later ...

You say it's the replica making the decisions, but my mental model is 
it's the publisher decoding the data for a given list of publications 
(which indeed is specified by the subscriber). But the subscriber can't 
tweak the definition of publications, right? Or what do you mean by 
queries executed by the replica? What are the gap?

> the logical-replication-feature to filter out columns.

Right. Although I haven't thought about how logical decoding interacts 
with column privileges. I don't think logical decoding actually checks 
column privileges - I certainly don't recall any ACL checks in 
src/backend/replication ...

AFAIK we only really check privileges during initial sync (when creating 
the slot and copying data), but then we keep replicating data even if 
the privilege gets revoked for the table/column. In principle the 
replication role is pretty close to superuser.

> I'm going to have a deeper look at this whole thing.

Does that mean we currently sync all the columns in the initial sync, 
and only start filtering columns later while decoding transactions?

regards

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 17 Dec 2021 22:34:16 -0300

On 2021-Dec-17, Tomas Vondra wrote:

> Interesting, I haven't really looked at this as a security feature. And in
> my experience if something is not carefully designed to be secure from the
> get go, it's really hard to add that bit later ...

I guess the way to really harden replication is to use the GRANT system
at the publisher's side to restrict access for the replication user.
This would provide actual security.  So you're right that I seem to be
barking at the wrong tree ...  maybe I need to give a careful look at
the documentation for logical replication to understand what is being
offered, and to make sure that we explicitly indicate that limiting the
column list does not provide any actual security.

> You say it's the replica making the decisions, but my mental model is it's
> the publisher decoding the data for a given list of publications (which
> indeed is specified by the subscriber). But the subscriber can't tweak the
> definition of publications, right? Or what do you mean by queries executed
> by the replica? What are the gap?

I am thinking in somebody modifying the code that the replica runs, so
that it ignores the column list that the publication has been configured
to provide; instead of querying only those columns, it would query all
columns.

> Right. Although I haven't thought about how logical decoding interacts with
> column privileges. I don't think logical decoding actually checks column
> privileges - I certainly don't recall any ACL checks in

Well, in practice if you're confronted with a replica that's controlled
by a malicious user that can tweak its behavior, then replica-side
privilege checking won't do anything useful.

> Does that mean we currently sync all the columns in the initial sync, and
> only start filtering columns later while decoding transactions?

No, it does filter the list of columns in the initial sync.  But the
current implementation is bogus, because it obtains the list of *all*
publications in which the table is published, not just the ones that the
subscription is configured to get data from.  And the sync code doesn't
receive the list of publications.  We need more thorough patching of the
sync code to close that hole.

---

From: Tomas Vondra <[email-masked]>
Date: Sat, 18 Dec 2021 02:59:00 +0100

> privilege checking won't do anything useful.

I don't follow. Surely the decoding happens on the primary node, right? 
Which is where the ACL checks would happen, using the role the 
replication connection is opened with.

> sync code to close that hole.

Ah, got it. Thanks for the explanation. Yeah, that makes no sense.

regards

---

From: Amit Kapila <[email-masked]>
Date: Sat, 18 Dec 2021 09:22:05 +0530

> column list does not provide any actual security.

IIRC, the use cases as mentioned by other databases (like Oracle) are
(a) this helps when the target table doesn't have the same set of
columns or (b) when the columns contain some sensitive information
like personal identification number, etc. I think there could be a
side benefit in this which comes from the fact that the lesser data
will flow across the network which could lead to faster replication
especially when the user filters large column data.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Sat, 18 Dec 2021 09:37:02 +0530

> columns in row filter expression are part of RI.

Another point related to RI is that this patch seems to restrict
specifying the RI columns in the column filter list irrespective of
publish action. Do we need to have such a restriction if the
publication publishes 'insert' or 'truncate'?

-- 
With Regards,
Amit Kapila.

---

From: Alvaro Herrera <[email-masked]>
Date: Sat, 18 Dec 2021 13:51:56 -0300

> > > > If the server has a *separate* security mechanism to hide the
> > > > columns (per-column privs), it is that feature that will protect
> > > > the data, not the logical-replication-feature to filter out
> > > > columns.
> > > 
> > > Right. Although I haven't thought about how logical decoding
> > > interacts with column privileges. I don't think logical decoding
> > > actually checks column privileges - I certainly don't recall any
> > > ACL checks in src/backend/replication ...
> > 
> > Well, in practice if you're confronted with a replica that's
> > controlled by a malicious user that can tweak its behavior, then
> > replica-side privilege checking won't do anything useful.
> 
> I don't follow. Surely the decoding happens on the primary node,
> right?  Which is where the ACL checks would happen, using the role the

I think you do follow.  Yes, the decoding happens on the primary node,
and the security checks should occur in the primary node, because to do
otherwise is folly(*).  Which means that column filtering, being a
replica-side feature, is *not* a security feature.  I was mistaken about
it, is all.  If you want security, you need to use column-level
privileges, as you say.

(*) The checks *must* occur in the primary side, because the primary
does not control the code that runs in the replica side.  The primary
must treat the replica as running potentially hostile code.  Trying to
defend against that is not practical.

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 20 Dec 2021 14:53:46 +0100

> uncooperative replica in order for the lot of data to be exposed anyway.

During normal replication, the publisher should only send the columns 
that are configured to be part of the publication.  So I don't see a 
problem there.

During the initial table sync, the subscriber indeed can construct any 
COPY command.  We could maybe replace this with a more customized COPY 
command variant, like COPY table OF publication TO STDOUT.

But right now the subscriber is sort of assumed to have access to 
everything on the publisher anyway, so I doubt that this is the only 
problem.  But it's worth considering.

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 27 Dec 2021 14:06:18 -0300

Determining that an array has a NULL element seems convoluted.  I ended
up with this query, where comparing the result of array_positions() with
an empty array does that.  If anybody knows of a simpler way, or any
situations in which this fails, I'm all ears.

with published_cols as (
        select case when
                pg_catalog.array_positions(pg_catalog.array_agg(unnest), null) <> '{}' then null else
                pg_catalog.array_agg(distinct unnest order by unnest) end AS attrs
        from pg_catalog.pg_publication p join
                pg_catalog.pg_publication_rel pr on (p.oid = pr.prpubid) left join
                unnest(prattrs) on (true)
        where prrelid = 38168 and p.pubname in ('pub1', 'pub2')
)
SELECT a.attname,
       a.atttypid,
       a.attnum = ANY(i.indkey)
  FROM pg_catalog.pg_attribute a
  LEFT JOIN pg_catalog.pg_index i
       ON (i.indexrelid = pg_get_replica_identity_index(38168)),
     published_cols
 WHERE a.attnum > 0::pg_catalog.int2
   AND NOT a.attisdropped and a.attgenerated = ''
   AND a.attrelid = 38168
   AND (published_cols.attrs IS NULL OR attnum = ANY(published_cols.attrs))
 ORDER BY a.attnum;

This returns all columns if at least one publication has a NULL prattrs,
or only the union of columns listed in all publications, if all
publications have a list of columns.

(I was worried about obtaining the list of publications, but it turns
out that it's already as a convenient list of OIDs in the MySubscription
struct.)

With this, we can remove the second query added by Rahila's original patch to
filter out nonpublished columns.

I still need to add pg_partition_tree() in order to search for
publications containing a partition ancestor.  I'm not yet sure what
happens (and what *should* happen) if an ancestor is part of a
publication and the partition is also part of a publication, and the
column lists differ.

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
Al principio era UNIX, y UNIX habló y dijo: "Hello world\n".
No dijo "Hello New Jersey\n", ni "Hello USA\n".

---

From: Tom Lane <[email-masked]>
Date: Mon, 27 Dec 2021 12:38:29 -0500

> situations in which this fails, I'm all ears.

Maybe better to rethink why we allow elements of prattrs to be null?

			regards, tom lane

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 27 Dec 2021 15:31:46 -0300

> Maybe better to rethink why we allow elements of prattrs to be null?

What I'm doing is an unnest of all arrays and then aggregating them
back into a single array.  If one array is null, the resulting aggregate
contains a null element.

Hmm, maybe I can in parallel do a bool_or() aggregate of "array is null" to
avoid that.  ... ah yes, that works:

with published_cols as (
        select pg_catalog.bool_or(pr.prattrs is null) as all_columns,
                pg_catalog.array_agg(distinct unnest order by unnest) AS attrs
        from pg_catalog.pg_publication p join
                pg_catalog.pg_publication_rel pr on (p.oid = pr.prpubid) left join
                unnest(prattrs) on (true)
        where prrelid = :table and p.pubname in ('pub1', 'pub2')
)
SELECT a.attname,
       a.atttypid,
       a.attnum = ANY(i.indkey)
  FROM pg_catalog.pg_attribute a
  LEFT JOIN pg_catalog.pg_index i
       ON (i.indexrelid = pg_get_replica_identity_index(:table)),
     published_cols
 WHERE a.attnum > 0::pg_catalog.int2
   AND NOT a.attisdropped and a.attgenerated = ''
   AND a.attrelid = :table
   AND (all_columns OR attnum = ANY(published_cols.attrs))
 ORDER BY a.attnum ;

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 28 Dec 2021 18:04:27 -0300

OK, getting closer now.  I've fixed the code to filter them column list
during the initial sync, and added some more tests for code that wasn't
covered.

There are still some XXX comments.  The one that bothers me most is the
lack of an implementation that allows changing the column list in a
publication without having to remove the table from the publication
first.

-- 
Álvaro Herrera           39°49'30"S 73°17'W  —  https://www.EnterpriseDB.com/
"I'm always right, but sometimes I'm more right than other times."
                                                  (Linus Torvalds)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 34a7034282..5bc2e7a591 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -6877,7 +6877,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 bb4ef5e5e2..c86055b93c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -30,7 +30,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r

 

+     <para>
index 62f10bcbd2..88e94a7cda 100644
@@ -46,12 +46,17 @@
 
 /*
- * 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 column specified as column filter, or NULL if
+ * no column filter was specified.
  */
 static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(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 +87,55 @@ check_publication_add_relation(Relation targetrel)
 				 errmsg("cannot add relation \"%s\" to publication",
 						RelationGetRelationName(targetrel)),
 				 errdetail("This operation is not supported for unlogged tables.")));
+
+	/*
+	 * Enforce that the column filter 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 (columns != NULL)
+	{
+		Bitmapset  *idattrs;
+		int			x;
+
+		/*
+		 * 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
+		 * that could 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 have column filter on relations with REPLICA IDENTITY FULL."));
+
+		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,9 +343,13 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	Oid			relid = RelationGetRelid(targetrel->relation);
 	Oid			prrelid;
 	Publication *pub = GetPublication(pubid);
+	Bitmapset  *attmap = NULL;
+	AttrNumber *attarray;
+	int			natts = 0;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
+	ListCell   *lc;

@@ -305,6 +363,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	{
 		table_close(rel, RowExclusiveLock);

@@ -314,7 +383,45 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 						RelationGetRelationName(targetrel->relation), pub->name)));
 	}
 
-	check_publication_add_relation(targetrel->relation);
+	/*
+	 * Translate list of columns to attnums. We prohibit system attributes and
+	 * make sure there are no duplicate columns.
+	 *
+	 * Note that the attribute numbers are *not* offset by
+	 * FirstLowInvalidHeapAttributeNumber; system columns are forbidden so this
+	 * should be okay.
+	 */
+	attarray = palloc(sizeof(AttrNumber) * list_length(targetrel->columns));
+	foreach(lc, targetrel->columns)
+	{
+		char	   *colname = strVal(lfirst(lc));
+		AttrNumber	attnum = get_attnum(relid, colname);
+
+		if (attnum == InvalidAttrNumber)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+						   colname, RelationGetRelationName(targetrel->relation)));
+
+		if (!AttrNumberIsForUserDefinedAttr(attnum))
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot reference system column \"%s\" in publication column list",
+						   colname));
+
+		if (bms_is_member(attnum, attmap))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("duplicate column \"%s\" in publication column list",
+						   colname));
+
+		attmap = bms_add_member(attmap, attnum);
+		attarray[natts++] = attnum;
+	}
+
+	check_publication_add_relation(targetrel->relation, attmap);
+
+	bms_free(attmap);
 
 	/* Form a tuple. */
@@ -327,6 +434,15 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,

@@ -334,8 +450,16 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	CatalogTupleInsert(rel, tup);
 	heap_freetuple(tup);
 
+	/* Register dependencies as needed */
 	ObjectAddressSet(myself, PublicationRelRelationId, prrelid);
 
+	/* 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);
@@ -470,6 +594,74 @@ GetRelationPublications(Oid relid)
 	return result;
 }
 
+/*
+ * Gets a list of OIDs of all column-partial 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;
+}
+
+/*
+ * 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 404bb5d0c8..cd2c6a0f70 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -561,7 +561,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);
 			}
 		}
@@ -932,6 +933,8 @@ OpenTableList(List *tables)

@@ -965,8 +968,11 @@ OpenTableList(List *tables)
 
@@ -1074,6 +1080,14 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 
+		/* XXX Shouldn't this be prevented by the grammar, ideally? Can it actually
+		 * happen? It does not seem to be tested in the regression tests. */
+		if (pubrel->columns)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("column list may not be specified for relation \"%s\" in ALTER PUBLICATION ... SET/DROP command",
+						   RelationGetRelationName(pubrel->relation)));
+
 		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 45e59e3d5c..a9051eb5e7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -40,8 +40,8 @@
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
-#include "catalog/pg_tablespace.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -8347,6 +8347,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;
@@ -8366,7 +8367,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)
@@ -8420,13 +8421,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)
 	{
@@ -8514,7 +8544,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);
 
@@ -15603,6 +15633,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)
 	{
@@ -15611,11 +15646,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 column-partial publications exist"));
 		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;
 	}
@@ -15626,7 +15666,6 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	else
 		elog(ERROR, "unexpected identity type %u", stmt->identity_type);
 
-
 	/* Check that the index exists */
 	indexOid = get_relname_relid(stmt->name, rel->rd_rel->relnamespace);
 	if (!OidIsValid(indexOid))
@@ -15701,6 +15740,38 @@ 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 column-partial publications.  All publications have to include all
+	 * key columns of the new index.
+	 */
+	foreach(lc, pubs)
+	{
+		Oid			pubid = lfirst_oid(lc);
+		List	   *published_cols;
+
+		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. */

 	return newnode;
 }
 
 	return true;
 }
index 9f5bf4b639..3428984130 100644
@@ -29,9 +29,11 @@
 
-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 */

 /*
@@ -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)

 
+	/* send number of live attributes */
+	for (i = 0; i < desc->natts; i++)
+	{
+		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);
+
 	/* 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 f07983a43c..1303e85851 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -111,6 +111,7 @@
 #include "replication/origin.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -697,17 +698,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 = false;
+	Bitmapset  *included_cols = NULL;
 
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -737,14 +741,18 @@ fetch_remote_table_info(char *nspname, char *relname,

-	/* 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"
@@ -772,16 +780,92 @@ 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 applicable column filter,
+	 * 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 column names only if they are contained in column filter
+	 * LogicalRepRelation will only contain attributes corresponding to those
+	 * specficied in column filters.
+	 */
 	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. */
@@ -791,12 +875,13 @@ 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);
 }
 
 /*
@@ -829,8 +914,17 @@ copy_table(Relation rel)
index 6f6a203dea..34df5d4956 100644
 
@@ -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);
@@ -130,6 +134,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. */
@@ -570,11 +581,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);
@@ -587,7 +598,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;
@@ -610,13 +622,17 @@ send_relation_and_attrs(Relation relation, TransactionId xid,
 		if (att->atttypid < FirstGenbkiObjectId)
 			continue;
 
+		/* Skip if attribute is not present in column filter. */
+		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);
 }
 
@@ -693,7 +709,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;
 			}
@@ -722,7 +738,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;
 			}
@@ -1122,6 +1138,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 	bool		am_partition = get_rel_relispartition(relid);
 	char		relkind = get_rel_relkind(relid);
 	bool		found;
+	Oid			ancestor_id;
 	MemoryContext oldctx;
 
 	Assert(RelationSyncCache != NULL);
@@ -1142,6 +1159,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 */
 	}
@@ -1182,6 +1200,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 		{
 			Publication *pub = lfirst(lc);
 			bool		publish = false;
+			bool		ancestor_published = false;
 
 			if (pub->alltables)
 			{
@@ -1192,8 +1211,6 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
 
 			if (!publish)
 			{
@@ -1219,6 +1236,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
@@ -1239,15 +1257,47 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
+							entry->columns = bms_add_member(entry->columns,
+															elems[i]);
 
 		list_free(pubids);
@@ -1343,6 +1393,8 @@ rel_sync_cache_relation_cb(Datum arg, Oid relid)
 		entry->schema_sent = false;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
+		bms_free(entry->columns);
+		entry->columns = NULL;
 		if (entry->map)
 		{
 			/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b52f3ccda2..d98b1b50c4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4034,6 +4034,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_oid;
 	int			i_prpubid;
 	int			i_prrelid;
+	int			i_prattrs;
 	int			i,
 				j,
 				ntups;
@@ -4045,8 +4046,13 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)

 	ntups = PQntuples(res);
@@ -4055,6 +4061,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -4096,6 +4103,28 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)

@@ -4160,10 +4189,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)

 
 	/*
 	 * 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 f011ace8a8..3f7500accc 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -630,6 +630,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 c28788e84f..b9d0ebf762 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5815,7 +5815,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;
@@ -5832,10 +5832,14 @@ addFooterToPublicationDesc(PQExpBuffer buf, char *footermsg,

@@ -5963,8 +5967,20 @@ describePublications(const char *pattern)
index cf30239f6d..25c7c08040 100644
index 902f2f2f0d..500991e696 100644
 
 extern Publication *GetPublication(Oid pubid);
@@ -109,6 +110,8 @@ 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 List *GetPublicationSchemas(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index b5d5504cbb..7ad285faae 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
+	int2vector	prattrs;		/* Variable length field starts here */
+#endif
 } FormData_pg_publication_rel;
 
 /* ----------------
 
 /*
index 83741dcf42..7a5cb9871d 100644
+									bool binary, Bitmapset *columns);
+									HeapTuple newtuple, bool binary, Bitmapset *columns);
+								 Relation rel, Bitmapset *columns);
index 5ac2d666a2..ae99b99cc6 100644
@@ -165,7 +165,22 @@ Publications:
 
+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.
@@ -669,6 +684,15 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_tes
 Tables from schemas:
     "pub_test1"
 
index 56dd358554..b422e3e374 100644
@@ -89,7 +89,17 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall

 CREATE TABLE testpub_tbl3 (a int);
@@ -362,6 +372,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/021_column_filter.pl b/src/test/subscription/t/021_column_filter.pl
new file mode 100644
index 0000000000..dfae6d8eac
--- /dev/null
+++ b/src/test/subscription/t/021_column_filter.pl
@@ -0,0 +1,164 @@
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# Test TRUNCATE
+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 column filtering
+$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 column filtering
+$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');

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 29 Dec 2021 21:15:08 -0300

> publication without having to remove the table from the publication

OK, I made some progress on this front; I added new forms of ALTER
PUBLICATION to support it:

ALTER PUBLICATION pub1 ALTER TABLE tbl SET COLUMNS (a, b, c);
ALTER PUBLICATION pub1 ALTER TABLE tbl SET COLUMNS ALL;

(not wedded to this syntax; other suggestions welcome)

In order to implement it I changed the haphazardly chosen use of
DEFELEM actions to a new enum.  I also noticed that the division of
labor between pg_publication.c and publicationcmds.c is quite broken
(code to translate column names to numbers is in the former, should be
in the latter; some code that deals with pg_publication tuples is in the
latter, should be in the former, such as CreatePublication,
AlterPublicationOptions).

This new stuff is not yet finished.  For example I didn't refactor
handling of REPLICA IDENTITY, so the new command does not correctly
check everything, such as the REPLICA IDENTITY FULL stuff.  Also, no
tests have been added yet.  In manual tests it seems to behave as
expected.

I noticed that prattrs is inserted in user-specified order instead of
catalog order, which is innocuous but quite weird.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"No renuncies a nada. No te aferres a nada."
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 34a7034282..5bc2e7a591 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -6877,7 +6877,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 bb4ef5e5e2..4951343f6f 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>

-    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 to change
+   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, departments;
+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 ALTER TABLE users SET COLUMNS (user_id, firstname, lastname);
 </programlisting></para>
 
   <para>

 
+     <para>
index 62f10bcbd2..322bfc2a82 100644
@@ -45,13 +45,23 @@
 #include "utils/rel.h"
 
+
+static void check_publication_columns(Relation targetrel, Bitmapset *columns);
+static AttrNumber *publication_translate_columns(Relation targetrel, List *columns,
+												 int *natts, Bitmapset **attset);
+
 /*
@@ -82,6 +92,62 @@ check_publication_add_relation(Relation targetrel)
+	/* Make sure the column list checks out */
+		check_publication_columns(targetrel, columns);
+check_publication_columns(Relation targetrel, Bitmapset *columns)
 
 /*
@@ -287,8 +353,11 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	Datum		values[Natts_pg_publication_rel];
 	bool		nulls[Natts_pg_publication_rel];
 	Oid			relid = RelationGetRelid(targetrel->relation);
-	Oid			prrelid;
+	Oid			pubreloid;
 	Publication *pub = GetPublication(pubid);
+	Bitmapset  *attset = NULL;
+	AttrNumber *attarray;
+	int			natts = 0;
 	ObjectAddress myself,
 				referenced;
 	List	   *relids = NIL;
@@ -314,19 +383,35 @@ 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 */
+	attarray = publication_translate_columns(targetrel->relation,
+											 targetrel->columns,
+											 &natts, &attset);
+
+	check_publication_add_relation(targetrel->relation, attset);
+
+	bms_free(attset);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
 
-	prrelid = GetNewOidWithIndex(rel, PublicationRelObjectIndexId,
-								 Anum_pg_publication_rel_oid);
-	values[Anum_pg_publication_rel_oid - 1] = ObjectIdGetDatum(prrelid);
+	pubreloid = GetNewOidWithIndex(rel, PublicationRelObjectIndexId,
+								   Anum_pg_publication_rel_oid);
+	values[Anum_pg_publication_rel_oid - 1] = ObjectIdGetDatum(pubreloid);
 	values[Anum_pg_publication_rel_prpubid - 1] =
 		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;

@@ -334,8 +419,16 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	CatalogTupleInsert(rel, tup);
 	heap_freetuple(tup);
 
-	ObjectAddressSet(myself, PublicationRelRelationId, prrelid);
+	/* Register dependencies as needed */
+	ObjectAddressSet(myself, PublicationRelRelationId, pubreloid);
 
@@ -363,6 +456,132 @@ publication_add_relation(Oid pubid, PublicationRelInfo *targetrel,
 	return myself;
 }
 
+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;
+
+		attarray = publication_translate_columns(targetrel, columns,
+												 &natts, &attset);
+
+		/* make sure the column list checks out */
+		/* XXX missing to check for the REPLICA IDENTITY FULL case */
+		/* XXX this should occur at caller in publicationcmds.c, not here */
+		check_publication_columns(targetrel, attset);
+
+		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);
+}
+
+/*
+ * 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 AttrNumber *
+publication_translate_columns(Relation targetrel, List *columns, int *natts,
+							  Bitmapset **attset)
+{
+	AttrNumber *attarray;
+	Bitmapset  *set = NULL;
+	ListCell   *lc;
+	int			n = 0;
+
+	/*
+	 * 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 (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;
+	}
+
+	/*
+	 * XXX qsort the array here, or maybe build just the bitmapset above and
+	 * then scan that in order to produce the array?  Do we care about the
+	 * array being unsorted?
+	 */
+
+	*natts = n;
+	*attset = set;
+	return attarray;
+}
+
 /*
  * Insert new publication / schema mapping.
  */
@@ -470,6 +689,74 @@ GetRelationPublications(Oid relid)
 	return result;
 }
 
+/*
index 404bb5d0c8..aefae8b3c4 100644
@@ -48,7 +48,7 @@
 
-static List *OpenReliIdList(List *relids);
+static List *OpenRelIdList(List *relids);
 static List *OpenTableList(List *tables);
 static void CloseTableList(List *rels);
 static void LockSchemaList(List *schemalist);
@@ -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.
  */
@@ -499,15 +539,16 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	Oid			pubid = pubform->oid;
 
 	/*
-	 * It is quite possible that for the SET case user has not specified any
-	 * tables in which case we need to remove all the existing tables.
+	 * Nothing to do if no objects, except in SET: for that it is quite
+	 * possible that user has not specified any schemas in which case we need
+	 * to remove all the existing schemas.
 	 */
-	if (!tables && stmt->action != DEFELEM_SET)
+	if (!tables && stmt->action != AP_SetObjects)
 		return;
 
 	rels = OpenTableList(tables);
 
-	if (stmt->action == DEFELEM_ADD)
+	if (stmt->action == AP_AddObjects)
 	{
 		List	   *schemas = NIL;
 
@@ -520,9 +561,17 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 											  PUBLICATIONOBJ_TABLE);
 		PublicationAddTables(pubid, rels, false, stmt);
 	}
-	else if (stmt->action == DEFELEM_DROP)
+	else if (stmt->action == AP_DropObjects)
 		PublicationDropTables(pubid, rels, false);
-	else						/* DEFELEM_SET */
+	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,
 														PUBLICATION_PART_ROOT);
@@ -561,7 +610,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 
@@ -593,10 +643,11 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 
 	/*
-	 * It is quite possible that for the SET case user has not specified any
-	 * schemas in which case we need to remove all the existing schemas.
+	 * Nothing to do if no objects, except in SET: for that it is quite
+	 * possible that user has not specified any schemas in which case we need
+	 * to remove all the existing schemas.
 	 */
-	if (!schemaidlist && stmt->action != DEFELEM_SET)
+	if (!schemaidlist && stmt->action != AP_SetObjects)
 		return;
 
 	/*
@@ -604,13 +655,13 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 	 * concurrent schema deletion.
 	 */
 	LockSchemaList(schemaidlist);
-	if (stmt->action == DEFELEM_ADD)
+	if (stmt->action == AP_AddObjects)
 	{
 		List	   *rels;
 		List	   *reloids;
 
 		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
-		rels = OpenReliIdList(reloids);
+		rels = OpenRelIdList(reloids);
 
 		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
 											  PUBLICATIONOBJ_TABLE_IN_SCHEMA);
@@ -618,9 +669,9 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		CloseTableList(rels);
 		PublicationAddSchemas(pubform->oid, schemaidlist, false, stmt);
 	}
-	else if (stmt->action == DEFELEM_DROP)
+	else if (stmt->action == AP_DropObjects)
 		PublicationDropSchemas(pubform->oid, schemaidlist, false);
-	else						/* DEFELEM_SET */
+	else if (stmt->action == AP_SetObjects)
 	{
 		List	   *oldschemaids = GetPublicationSchemas(pubform->oid);
 		List	   *delschemas = NIL;
@@ -643,6 +694,10 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		 */
 		PublicationAddSchemas(pubform->oid, schemaidlist, true, stmt);
 	}
+	else
+	{
+		/* Nothing to do for AP_SetColumns */
+	}
 }
 
 /*
@@ -655,7 +710,7 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	if ((stmt->action == DEFELEM_ADD || stmt->action == DEFELEM_SET) &&
+	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -868,7 +923,7 @@ RemovePublicationSchemaById(Oid psoid)
  * add them to a publication.
  */
 static List *
-OpenReliIdList(List *relids)
+OpenRelIdList(List *relids)
 {
 	ListCell   *lc;
 	List	   *rels = NIL;
@@ -932,6 +987,8 @@ OpenTableList(List *tables)

@@ -965,8 +1022,11 @@ OpenTableList(List *tables)
 
@@ -1074,6 +1134,11 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 
+		if (pubrel->columns)
+			ereport(ERROR,
+					errmsg("column list must not be specified in ALTER PUBLICATION ... DROP"));
 
 	ReleaseSysCache(tuple);
 
+	/*
 
 	if (children)
 	{

 
 
-

 
 	return newnode;
 }
 
 	return true;
 }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3d4dd43e47..68b1136788 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9742,12 +9742,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
 				{
@@ -9762,28 +9763,38 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE_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
 				{
@@ -9809,6 +9820,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 [, ...]
@@ -9830,7 +9844,7 @@ AlterPublicationStmt:
 					n->pubname = $3;
 					n->pubobjects = $5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
-					n->action = DEFELEM_ADD;
+					n->action = AP_AddObjects;
 					$$ = (Node *)n;
 				}
 			| ALTER PUBLICATION name SET pub_obj_list
@@ -9839,16 +9853,42 @@ AlterPublicationStmt:
 					n->pubname = $3;
 					n->pubobjects = $5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
-					n->action = DEFELEM_SET;
+					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);
 					n->pubname = $3;
 					n->pubobjects = $5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
-					n->action = DEFELEM_DROP;
+					n->action = AP_DropObjects;
 					$$ = (Node *)n;
 				}
 		;
@@ -17435,8 +17475,9 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 			{
 				/* convert it to PublicationTable */
 				PublicationTable *pubtable = makeNode(PublicationTable);
-				pubtable->relation = makeRangeVar(NULL, pubobj->name,
-												  pubobj->location);
+
+				pubtable->relation =
+					makeRangeVar(NULL, pubobj->name, pubobj->location);
 				pubobj->pubtable = pubtable;
 				pubobj->name = NULL;
 			}
@@ -17444,6 +17485,16 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLE_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 9f5bf4b639..3428984130 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)

 

 /*

 
 /*

 /*

 
 /*

 

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

 
 
+	/*

 		ExecClearTuple(slot);
 	}
+
 
 	lrel->natts = natt;

 /*

 

 	if (in_streaming)

 

 
 
 			if (pub->alltables)
 			{
 
 			if (!publish)
 			{
 
 		list_free(pubids);

 	ntups = PQntuples(res);

 

 
 	/*
 
 /*

index 902f2f2f0d..edd4f0c63c 100644

 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 List *GetPublicationSchemas(Oid pubid);
@@ -127,6 +130,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 char *get_publication_name(Oid pubid, bool missing_ok);
 
 /* ----------------
index 4c5a8a39bf..91ea815e14 100644
 
 /*
@@ -3674,6 +3675,14 @@ typedef struct CreatePublicationStmt
 	bool		for_all_tables; /* Special publication for all tables in db */
 } CreatePublicationStmt;
 
+typedef enum AlterPublicationAction
+{
+	AP_AddObjects,				/* add objects to publication */
+	AP_DropObjects,				/* remove objects from publication */
+	AP_SetObjects,				/* set list of objects */
+	AP_SetColumns				/* change list of columns for a table */
+} AlterPublicationAction;
+
 typedef struct AlterPublicationStmt
 {
 	NodeTag		type;
@@ -3688,7 +3697,7 @@ typedef struct AlterPublicationStmt
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
 	bool		for_all_tables; /* Special publication for all tables in db */
-	DefElemAction action;		/* What action to perform with the
+	AlterPublicationAction action; /* What action to perform with the
 								 * tables/schemas */
 } AlterPublicationStmt;
 
index 5ac2d666a2..9f540e1144 100644
@@ -165,7 +165,24 @@ Publications:
 
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5 (a);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
@@ -669,6 +686,15 @@ ALTER PUBLICATION testpub1_forschema SET ALL TABLES IN SCHEMA pub_test1, pub_tes
 Tables from schemas:
     "pub_test1"
 
index 56dd358554..d82b034efd 100644
@@ -89,7 +89,18 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 
+ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5 (a);
 
 CREATE TABLE testpub_tbl3 (a int);
@@ -362,6 +373,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: Alvaro Herrera <[email-masked]>
Date: Thu, 30 Dec 2021 17:21:28 -0300

> tests have been added yet.  In manual tests it seems to behave as

Fixing the lack of check for replica identity full didn't really require
much refactoring, so I did it that way.

I split it with some trivial fixes that can be committed separately
ahead of time.  I'm thinking in committing 0001 later today, perhaps
0002 tomorrow.  The interesting part is 0003.

---

From: Justin Pryzby <[email-masked]>
Date: Thu, 30 Dec 2021 16:16:34 -0600

> +	am_partition = DatumGetChar(slot_getattr(slot, 4, &isnull));

I think this needs to be GetBool.
You should Assert(!isnull) like the others.
Also, I think it doesn't need to be initialized to "false".

> +		 * that could would have to be included (because of being part of the

could would is wrong

> +	 * make sure there are no duplicate columns.

extraneous line

> + * relation, that is, those that specify a column list.

I would call this a "partial-column" publication.

> +	 * Check column-partial publications.  All publications have to include all

same

> +	 * Store the column names only if they are contained in column filter

period(.)

> +	 * specficied in column filters.

specified

> +	int2vector	prattrs;		/* Variable length field starts here */

The language in the pre-existing comments is better:
	/* variable-length fields start here */

> -       walrcv_clear_result(res);

The blank line after "lrel->natts = natt;" should be removed.

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 30 Dec 2021 20:32:29 -0300

On 2021-Dec-30, Justin Pryzby wrote:

Thank you!  I've incorporated your proposed fixes.

> > +		 * that could would have to be included (because of being part of the

Hah, yeah, this was "that column would".

> I would call this a "partial-column" publication.

OK, done that way.

---

From: Justin Pryzby <[email-masked]>
Date: Fri, 31 Dec 2021 10:32:27 -0600

>  							  "WHERE c.relnamespace = n.oid\n"

I suppose this should use pr.prattrs::pg_catalog.int2[] ?

Did the DatumGetBool issue expose a deficiency in testing ?
I think the !am_partition path was never being hit.

-- 
Justin

---

From: Peter Smith <[email-masked]>
Date: Mon, 25 Jul 2022 17:57:09 +1000

...
> Thanks.  I hadn't looked at the docs yet, so I'll definitely take this.

Was this documentation ever written?

My assumption was that for PG15 there might be a whole new section
added to Chapter 31 [1] for describing "Column Lists" (i.e. the Column
List equivalent of the "Row Filters" section)

------
[1] https://www.postgresql.org/docs/15/logical-replication.html

Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Amit Kapila <[email-masked]>
Date: Tue, 2 Aug 2022 14:27:38 +0530

> List equivalent of the "Row Filters" section)

+1. I think it makes sense to give more description about this feature
similar to Row Filters. Note that apart from the main feature commit
[1], we have prohibited certain cases in commit [2]. So, one might
want to cover that as well.

[1] - https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=923def9a533a7d986acfb524139d8b9e5466d0a5
[2] - https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=fd0b9dcebda7b931a41ce5c8e86d13f2efd0af2e

-- 
With Regards,
Amit Kapila.

---

From: Peter Smith <[email-masked]>
Date: Wed, 3 Aug 2022 08:42:01 +1000

> [2] - https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=fd0b9dcebda7b931a41ce5c8e86d13f2efd0af2e

OK. Unless somebody else has already started this work then I can do
this. I will post a draft patch in a few days.

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Peter Smith <[email-masked]>
Date: Mon, 8 Aug 2022 18:37:51 +1000

PSA patch version v1* for a new "Column Lists" pgdocs section

This is just a first draft, but I wanted to post it as-is, with the
hope that I can get some feedback while continuing to work on it.

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: vignesh C <[email-masked]>
Date: Tue, 16 Aug 2022 22:34:26 +0530

> hope that I can get some feedback while continuing to work on it.

Few comments:
1) Row filters mentions that "It has no effect on TRUNCATE commands.",
the same is not present in case of column filters. We should keep the
changes similarly for consistency.
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -90,8 +90,7 @@ CREATE PUBLICATION <replaceable
class="parameter">name</replaceable>
      <para>
       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.
+      through this publication, including any columns added later.

2) The document says that "if the table uses REPLICA IDENTITY FULL,
specifying a column list is not allowed.":
+   publishes only <command>INSERT</command> operations. Furthermore, if the
+   table uses <literal>REPLICA IDENTITY FULL</literal>, specifying a column
+   list is not allowed.
+  </para>

Did you mean specifying a column list during create publication for
REPLICA IDENTITY FULL table like below scenario:
postgres=# create table t2(c1 int, c2 int, c3 int);
CREATE TABLE
postgres=# alter table t2 replica identity full ;
ALTER TABLE
postgres=# create publication pub1 for table t2(c1,c2);
CREATE PUBLICATION

If so, the document says specifying column list is not allowed, but
creating a publication with column list on replica identity full was
successful.

Regards,
Vignesh

---

From: Peter Smith <[email-masked]>
Date: Mon, 22 Aug 2022 18:27:43 +1000

Thanks for the view of v1-0001.

On Wed, Aug 17, 2022 at 3:04 AM vignesh C <[email-masked]> wrote:
...
> +      through this publication, including any columns added later.

Modified as suggested.

> creating a publication with column list on replica identity full was

That patch v1-0001 was using the same wording from the github commit
message [1]. I agree it was a bit vague.

In fact the replica identity validation is done at DML execution time
so your example will fail as expected when you attempt to do a UPDATE
operation.

e.g.
test_pub=# update t2 set c2=23 where c1=1;
ERROR:  cannot update table "t2"
DETAIL:  Column list used by the publication does not cover the
replica identity.

I modified the wording for this part of the docs.

~~~

PSA new set of v2* patches.

------
[1] - https://github.com/postgres/postgres/commit/923def9a533a7d986acfb524139d8b9e5466d0a5

Kind Regards,
Peter Smith
Fujitsu Australia

---

From: Erik Rijkers <[email-masked]>
Date: Mon, 22 Aug 2022 11:11:46 +0200

Hi,

In the second file a small typo, I think:

"enclosed by parenthesis"  should be
"enclosed by parentheses"

thanks,
Erik

---

From: vignesh C <[email-masked]>
Date: Mon, 22 Aug 2022 16:55:19 +0530

> I modified the wording for this part of the docs.

Few comments:
1) I felt no expressions are allowed in case of column filters. Only
column names can be specified. The second part of the sentence
confuses what is allowed and what is not allowed. Won't it be better
to remove the second sentence and mention that only column names can
be specified.
+   <para>
+    Column list can contain only simple column references. Complex
+    expressions, function calls etc. are not allowed.
+   </para>

2) tablename should be table name.
+   <para>
+    A column list is specified per table following the tablename, and
enclosed by
+    parenthesis. See <xref linkend="sql-createpublication"/> for details.
+   </para>

We have used table name in the same page in other instances like:
a) The row filter is defined per table. Use a WHERE clause after the
table name for each published table that requires data to be filtered
out. The WHERE clause must be enclosed by parentheses.
b) The tables are matched between the publisher and the subscriber
using the fully qualified table name.

3) One small whitespace issue:
git am v2-0001-Column-List-replica-identity-rules.patch
Applying: Column List replica identity rules.
.git/rebase-apply/patch:30: trailing whitespace.
   if the publication publishes only <command>INSERT</command> operations.
warning: 1 line adds whitespace errors.

Regards,
Vignesh

---

From: Peter Smith <[email-masked]>
Date: Tue, 23 Aug 2022 12:22:00 +1000

On Mon, Aug 22, 2022 at 9:25 PM vignesh C <[email-masked]> wrote:
...

> +    expressions, function calls etc. are not allowed.

This wording was lifted verbatim from the commit message [1]. But I
see your point that it just seems to be overcomplicating a simple
rule. Modified as suggested.

> using the fully qualified table name.

Fixed as suggested.

> warning: 1 line adds whitespace errors.

Fixed.

~~~

PSA the v3* patch set.

------
[1] https://github.com/postgres/postgres/commit/923def9a533a7d986acfb524139d8b9e5466d0a5

Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Peter Smith <[email-masked]>
Date: Tue, 23 Aug 2022 12:25:38 +1000

> "enclosed by parentheses"

Thanks for your feedback.

Fixed in the v3* patches [1].

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

Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: vignesh C <[email-masked]>
Date: Thu, 25 Aug 2022 15:08:39 +0530

> > warning: 1 line adds whitespace errors.

Thanks for the updated patch.
Few comments:
1) We can shuffle the columns in publisher table and subscriber to
show that the order of the column does not matter
+   <para>
+    Create a publication <literal>p1</literal>. A column list is defined for
+    table <literal>t1</literal> to reduce the number of columns that will be
+    replicated.
+<programlisting>
+test_pub=# CREATE PUBLICATION p1 FOR TABLE t1 (id, a, b, c);
+CREATE PUBLICATION
+test_pub=#
+</programlisting></para>

2) We can try to keep the line content to less than 80 chars
+   <para>
+    A column list is specified per table following the tablename, and
enclosed by
+    parenthesis. See <xref linkend="sql-createpublication"/> for details.
+   </para>

3) tablename should be table name like it is used in other places
+   <para>
+    A column list is specified per table following the tablename, and
enclosed by
+    parenthesis. See <xref linkend="sql-createpublication"/> for details.
+   </para>

4a) In the below, you could include mentioning "Only the column list
data of publication <literal>p1</literal> are replicated."
+    <para>
+     Insert some rows to table <literal>t1</literal>.
+<programlisting>
+test_pub=# INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+INSERT 0 1

4b) In the above, we could mention that the insert should be done on
the "publisher side" as the previous statements are executed on the
subscriber side.

Regards,
Vignesh

---

From: Peter Smith <[email-masked]>
Date: Fri, 26 Aug 2022 12:02:57 +1000

On Thu, Aug 25, 2022 at 7:38 PM vignesh C <[email-masked]> wrote:
...
> +</programlisting></para>

OK. I made the following changes to the example.
- now the subscriber table defines cols in a different order than that
of the publisher table
- now the publisher column list defines col names in a different order
than that of the table
- now the column list avoids using only adjacent column names

> +    parenthesis. See <xref linkend="sql-createpublication"/> for details.

OK. Modified to use < 80 chars

> +    parenthesis. See <xref linkend="sql-createpublication"/> for details.

Sorry, I don't see this problem. AFAIK this same issue was already
fixed in the v3* patches.  Notice in the cited fragment that
'parenthesis' is misspelt but that was also fixed in v3. Maybe you are
looking at an old patch file (??)

> +test_pub=# INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');

OK. Modified to say this.

> the "publisher side" as the previous statements are executed on the

OK. Modified to say this.

~~~

Thanks for the feedback.

PSA patch set v4* where all of the above comments are now addressed.

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Amit Kapila <[email-masked]>
Date: Thu, 1 Sep 2022 15:23:30 +0530

On Fri, Aug 26, 2022 at 7:33 AM Peter Smith <[email-masked]> wrote:

Few comments on both the patches:
v4-0001*
=========
1.
Furthermore, if the table uses
+   <literal>REPLICA IDENTITY FULL</literal>, specifying a column list is not
+   allowed (it will cause publication errors for <command>UPDATE</command> or
+   <command>DELETE</command> operations).

This line sounds a bit unclear to me. From this like it appears that
the following operation is not allowed:

postgres=# create table t1(c1 int, c2 int, c3 int);
CREATE TABLE
postgres=# Alter Table t1 replica identity full;
ALTER TABLE
postgres=# create publication pub1 for table t1(c1);
CREATE PUBLICATION

However, what is not allowed is the following:
postgres=# delete from t1;
ERROR:  cannot delete from table "t1"
DETAIL:  Column list used by the publication does not cover the
replica identity.

I am not sure if we really need this line but if so then please try to
make it more clear. I think the similar text is present in 0002 patch
which should be modified accordingly.

V4-0002*
=========
2.
However, if a
+   <firstterm>column list</firstterm> is specified then only the columns named
+   in the list will be replicated. This means the subscriber-side table only
+   needs to have those columns named by the column list. A user might choose to
+   use column lists for behavioral, security or performance reasons.
+  </para>
+
+  <sect2 id="logical-replication-col-list-rules">
+   <title>Column List Rules</title>
+
+   <para>
+    A column list is specified per table following the table name, and enclosed
+    by parentheses. See <xref linkend="sql-createpublication"/> for details.
+   </para>
+
+   <para>
+    When a column list is specified, only the named columns are replicated.
+    The list order is not important.

It seems like "When a column list is specified, only the named columns
are replicated." is almost a duplicate of the line in the first para.
So, I think we can remove it. And if we do so then the second line
could be changed to something like: "While specifying column list, the
order of columns is not important."

3. It seems information about initial table synchronization is
missing. We copy only columns specified in the column list. Also, it
would be good to add a Note similar to Row Filter to indicate that
this list won't be used by pre-15 publishers.

-- 
With Regards,
Amit Kapila.

---

From: Peter Smith <[email-masked]>
Date: Fri, 2 Sep 2022 13:15:33 +1000

> which should be modified accordingly.

The "Furthermore…" sentence came from the commit message [1]. But I
agree it seems redundant/ambiguous, so I have removed it (and removed
the same in patch 0002).

Modified as suggested.

> this list won't be used by pre-15 publishers.

Done as suggested. Added a new "Initial Data Synchronization" section
with content similar to that of the Row Filters section.

~~~

Thanks for your review comments.

PSA v5* patches where all the above have been addressed.

------

Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Amit Kapila <[email-masked]>
Date: Fri, 2 Sep 2022 19:10:02 +0530

> agree it seems redundant/ambiguous, so I have removed it (and removed

Thanks, pushed your first patch.

-- 
With Regards,
Amit Kapila.

---

From: Peter Smith <[email-masked]>
Date: Mon, 5 Sep 2022 10:28:11 +1000

> Thanks, pushed your first patch.

Thanks for the push.

I have rebased the remaining patch (v6-0001 is the same as v5-0002)

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: "[email-masked]" <[email-masked]>
Date: Mon, 5 Sep 2022 03:42:40 +0000

> I have rebased the remaining patch (v6-0001 is the same as v5-0002)

Thanks for updating the patch. Here are some comments.

1.
+     the <xref linkend="sql-alterpublication"/> will be successful but later
+     the WalSender on the publisher, or the subscriber may throw an error. In
+     this scenario, the user needs to recreate the subscription after adjusting

Should "WalSender" be changed to "walsender"? I saw "walsender" is used in other
places in the documentation.

2.
+test_pub=# CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+CREATE TABLE
+test_pub=#

+test_pub=# CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+CREATE PUBLICATION
+test_pub=#

I think the redundant "test_pub=#" can be removed.

Besides, I tested the examples in the patch, there's no problem.

Regards,
Shi yu

---

From: Peter Smith <[email-masked]>
Date: Mon, 5 Sep 2022 20:16:09 +1000

> places in the documentation.

Modified.

> I think the redundant "test_pub=#" can be removed.

Modified.

> Besides, I tested the examples in the patch, there's no problem.

Thanks for the review comments, and testing.

I made both fixes as suggested.

PSA v7.

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Amit Kapila <[email-masked]>
Date: Mon, 5 Sep 2022 16:16:10 +0530

> PSA v7.

For example, if additional columns are added to the table, then
+    (after a <literal>REFRESH PUBLICATION</literal>) if there was a column list
+    only those named columns will continue to be replicated.

This looks a bit unclear to me w.r.t the refresh publication step. Why
exactly you have used refresh publication in the above para? It is
used to add new tables if any added to the publication, so not clear
to me how it helps in this case. If that is not required then we can
change it to: "For example, if additional columns are added to the
table then only those named columns mentioned in the column list will
continue to be replicated."

-- 
With Regards,
Amit Kapila.

---

From: Peter Smith <[email-masked]>
Date: Tue, 6 Sep 2022 09:37:45 +1000

> continue to be replicated."

You are right - that REFRESH PUBLICATION was not necessary for this
example. The patch is modified to use your suggested text.

PSA v8

------
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Amit Kapila <[email-masked]>
Date: Tue, 6 Sep 2022 14:15:22 +0530

> example. The patch is modified to use your suggested text.

LGTM. I'll push this once the tag appears for v15.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Wed, 7 Sep 2022 16:18:48 +0530

> LGTM. I'll push this once the tag appears for v15.

Pushed!

-- 
With Regards,
Amit Kapila.

---

From: Peter Smith <[email-masked]>
Date: Thu, 8 Sep 2022 08:31:52 +1000

> > LGTM. I'll push this once the tag appears for v15.

Thanks for pushing.

------
Kind Regards,
Peter Smith.
Fujitsu Australia.