Skipping schema changes in publication

Started by vignesh Calmost 4 years ago173 messages
#1vignesh C
vignesh21@gmail.com
1 attachment(s)

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.
This feature is for the pg16 version.
Thoughts?

Regards,
Vignesh

Attachments:

v1-0001-Skip-publishing-the-tables-of-schema.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Skip-publishing-the-tables-of-schema.patchDownload
From 153e033a78ace66bfbe1cd37db2f3de506740bcb Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Fri, 18 Mar 2022 10:41:35 +0530
Subject: [PATCH v1] Skip publishing the tables of schema.

A new option "SKIP ALL TABLES IN SCHEMA" in Create/Alter Publication allows
one or more skip schemas to be specified, publisher will skip sending the data
of the tables present in the skip schema to the subscriber.

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to maintain
the schemas that the user wants to skip publishing through the publication.
Modified the output plugin (pgoutput) to skip publishing the changes if the
relation is part of skip schema publication.

Updates pg_dump to identify and dump skip schema publications. Updates the \d
family of commands to display skip schema publications and \dRp+ variant will
now display associated skip schemas if any.
---
 doc/src/sgml/catalogs.sgml                    |   9 ++
 doc/src/sgml/logical-replication.sgml         |   7 +-
 doc/src/sgml/ref/alter_publication.sgml       |  28 +++-
 doc/src/sgml/ref/create_publication.sgml      |  28 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  66 +++++++---
 src/backend/commands/publicationcmds.c        | 123 +++++++++++-------
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/nodes/copyfuncs.c                 |  14 ++
 src/backend/nodes/equalfuncs.c                |  14 ++
 src/backend/parser/gram.y                     |  99 +++++++++++++-
 src/backend/replication/pgoutput/pgoutput.c   |  24 +---
 src/backend/utils/cache/relcache.c            |  22 +++-
 src/bin/pg_dump/pg_dump.c                     |  33 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 +++++
 src/bin/psql/describe.c                       |  17 +++
 src/bin/psql/tab-complete.c                   |  25 ++--
 src/include/catalog/pg_publication.h          |  20 ++-
 .../catalog/pg_publication_namespace.h        |   1 +
 src/include/commands/publicationcmds.h        |   3 +-
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/test/regress/expected/publication.out     |  84 +++++++++++-
 src/test/regress/sql/publication.sql          |  41 +++++-
 .../t/030_rep_changes_skip_schema.pl          |  96 ++++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 28 files changed, 678 insertions(+), 124 deletions(-)
 create mode 100644 src/test/subscription/t/030_rep_changes_skip_schema.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2a8cd02664..18e3cf82aa 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6281,6 +6281,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        Reference to schema
       </para></entry>
      </row>
+
+    <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pnskip</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the schema is skip schema
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 555fbd749c..e2a4b89226 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -599,9 +599,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
 
   <para>
    To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   table. To add all tables in schema or skip all tables in schema to a
+   publication, the user must be a superuser. To create a publication that
+   publishes all tables or all tables in schema automatically, the user must be
+   a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 32b75f6c78..8466e94ab0 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -31,7 +31,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
-    ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+    [SKIP] ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -71,12 +71,12 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
+   The <literal>ADD [SKIP] ALL TABLES IN SCHEMA</literal> and
+   <literal>SET [SKIP] ALL TABLES IN SCHEMA</literal> to a publication requires
+   the invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
    <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
+   of a <literal>FOR ALL TABLES</literal> or <literal>FOR [SKIP] ALL TABLES IN
    SCHEMA</literal> publication must be a superuser. However, a superuser can
    change the ownership of a publication regardless of these restrictions.
   </para>
@@ -88,6 +88,14 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    adding/setting a table to a publication that already has a table's schema as
    part of the specified schema is not supported.
   </para>
+
+  <para>
+   The <literal>ADD SKIP ALL TABLES IN SCHEMA</literal> and
+   <literal>SET SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+   <literal>FOR ALL TABLES</literal> publication. It is not supported for
+   <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+   <literal>FOR TABLE</literal> publication.
+  </para>
  </refsect1>
 
  <refsect1>
@@ -195,6 +203,16 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
 </programlisting>
   </para>
+
+   <para>
+   Add skip schemas <structname>sales_june</structname> and
+   <structname>sales_july</structname> to the publication
+   <structname>mypublication</structname>:
+<programlisting>
+ALTER PUBLICATION mypublication ADD SKIP ALL TABLES IN SCHEMA sales_june, sales_july;
+</programlisting>
+  </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 4979b9b646..03d8e82eec 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA }]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -117,6 +117,23 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>SKIP ALL TABLES IN SCHEMA</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that skips replicating changes for all
+      tables in the specified list of schemas.
+     </para>
+
+     <para>
+      <literal>SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>FOR ALL TABLES IN SCHEMA</literal></term>
     <listitem>
@@ -327,6 +344,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
    <structname>sales</structname>:
 <programlisting>
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of all the tables present in the schema
+   <structname>marketing</structname> and <structname>sales</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE SKIP ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index caabb06c53..4ba4140933 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1856,8 +1856,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        schemas and the skip schema associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 789b895db8..711dda864f 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -287,7 +287,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -301,6 +302,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		Oid			ancestor = lfirst_oid(lc);
 		List	   *apubids = GetRelationPublications(ancestor);
 		List	   *aschemaPubids = NIL;
+		List       *askipschemaPubids = NIL;
 
 		level++;
 
@@ -313,8 +315,10 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		}
 		else
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor), false);
+			askipschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor), true);
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(askipschemaPubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -436,13 +440,14 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
  * Insert new publication / schema mapping.
  */
 ObjectAddress
-publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
+publication_add_schema(Oid pubid, PublicationSchInfo *pubsch, bool if_not_exists)
 {
 	Relation	rel;
 	HeapTuple	tup;
 	Datum		values[Natts_pg_publication_namespace];
 	bool		nulls[Natts_pg_publication_namespace];
 	Oid			psschid;
+	Oid			schemaid = pubsch->oid;
 	Publication *pub = GetPublication(pubid);
 	List	   *schemaRels = NIL;
 	ObjectAddress myself,
@@ -483,6 +488,8 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_namespace_pnnspid - 1] =
 		ObjectIdGetDatum(schemaid);
+	values[Anum_pg_publication_namespace_pnskip - 1] =
+		BoolGetDatum(pubsch->skip);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -630,13 +637,23 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *skipschemaidlist = NIL;
+	List	   *pubschemalist = GetPublicationSchemas(pubid);
+	ListCell   *cell;
+
+	foreach(cell, pubschemalist)
+	{
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
+
+		skipschemaidlist = lappend_oid(result, pubsch->oid);
+	}
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -651,9 +668,11 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	{
 		Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 		Oid			relid = relForm->oid;
+		Oid			schid = get_rel_namespace(relid);
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(skipschemaidlist, schid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -672,9 +691,11 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		{
 			Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 			Oid			relid = relForm->oid;
+			Oid			schid = get_rel_namespace(relid);
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(skipschemaidlist, schid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -713,10 +734,14 @@ GetPublicationSchemas(Oid pubid)
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_namespace pubsch;
+		PublicationSchInfo *schinfo = makeNode(PublicationSchInfo);
+
 
 		pubsch = (Form_pg_publication_namespace) GETSTRUCT(tup);
+		schinfo->oid = pubsch->pnnspid;
+		schinfo->skip = pubsch->pnskip;
 
-		result = lappend_oid(result, pubsch->pnnspid);
+		result = lappend(result, schinfo);
 	}
 
 	systable_endscan(scan);
@@ -729,7 +754,7 @@ GetPublicationSchemas(Oid pubid)
  * Gets the list of publication oids associated with a specified schema.
  */
 List *
-GetSchemaPublications(Oid schemaid)
+GetSchemaPublications(Oid schemaid, bool skippub)
 {
 	List	   *result = NIL;
 	CatCList   *pubschlist;
@@ -743,7 +768,8 @@ GetSchemaPublications(Oid schemaid)
 		HeapTuple	tup = &pubschlist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_namespace) GETSTRUCT(tup))->pnpubid;
 
-		result = lappend_oid(result, pubid);
+		if (skippub == ((Form_pg_publication_namespace) GETSTRUCT(tup))->pnskip)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubschlist);
@@ -812,7 +838,8 @@ GetSchemaPublicationRelations(Oid schemaid, PublicationPartOpt pub_partopt)
  * publication.
  */
 List *
-GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+								 bool bskip)
 {
 	List	   *result = NIL;
 	List	   *pubschemalist = GetPublicationSchemas(pubid);
@@ -820,11 +847,16 @@ GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 
 	foreach(cell, pubschemalist)
 	{
-		Oid			schemaid = lfirst_oid(cell);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
 		List	   *schemaRels = NIL;
 
-		schemaRels = GetSchemaPublicationRelations(schemaid, pub_partopt);
-		result = list_concat(result, schemaRels);
+
+		/* Skip the skip publication schemas if bskip is true */
+		if (bskip && !pubsch->skip)
+		{
+			schemaRels = GetSchemaPublicationRelations(pubsch->oid, pub_partopt);
+			result = list_concat(result, schemaRels);
+		}
 	}
 
 	return result;
@@ -958,7 +990,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
@@ -972,7 +1005,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			schemarelids = GetAllSchemaPublicationRelations(publication->oid,
 															publication->pubviaroot ?
 															PUBLICATION_PART_ROOT :
-															PUBLICATION_PART_LEAF);
+															PUBLICATION_PART_LEAF,
+															true);
 			tables = list_concat_unique_oid(relids, schemarelids);
 
 			/*
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1aad2e769c..8fd63c01a9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -177,8 +177,8 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 
 	foreach(cell, pubobjspec_list)
 	{
-		Oid			schemaid;
 		List	   *search_path;
+		PublicationSchInfo *pubsch = makeNode(PublicationSchInfo);
 
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
@@ -188,10 +188,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
-				schemaid = get_namespace_oid(pubobj->name, false);
+				pubsch->oid = get_namespace_oid(pubobj->name, false);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*schemas = list_append_unique_oid(*schemas, schemaid);
+				*schemas = list_append_unique(*schemas, pubsch);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA:
 				search_path = fetch_search_path(false);
@@ -200,11 +201,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 							errcode(ERRCODE_UNDEFINED_SCHEMA),
 							errmsg("no schema has been selected for CURRENT_SCHEMA"));
 
-				schemaid = linitial_oid(search_path);
+				pubsch->oid = linitial_oid(search_path);
 				list_free(search_path);
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*schemas = list_append_unique_oid(*schemas, schemaid);
+				*schemas = list_append_unique(*schemas, pubsch);
 				break;
 			default:
 				/* shouldn't happen */
@@ -230,24 +231,29 @@ CheckObjSchemaNotAlreadyInPublication(List *rels, List *schemaidlist,
 		Relation	rel = pub_rel->relation;
 		Oid			relSchemaId = RelationGetNamespace(rel);
 
-		if (list_member_oid(schemaidlist, relSchemaId))
+		foreach(lc, schemaidlist)
 		{
-			if (checkobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add schema \"%s\" to publication",
-							   get_namespace_name(relSchemaId)),
-						errdetail("Table \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
-								  RelationGetRelationName(rel),
-								  get_namespace_name(relSchemaId)));
-			else if (checkobjtype == PUBLICATIONOBJ_TABLE)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add relation \"%s.%s\" to publication",
-							   get_namespace_name(relSchemaId),
-							   RelationGetRelationName(rel)),
-						errdetail("Table's schema \"%s\" is already part of the publication or part of the specified schema list.",
-								  get_namespace_name(relSchemaId)));
+			PublicationSchInfo *pub_sch = (PublicationSchInfo *) lfirst(lc);
+
+			if (pub_sch->oid == relSchemaId)
+			{
+				if (checkobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add schema \"%s\" to publication",
+								   get_namespace_name(relSchemaId)),
+							errdetail("Table \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
+									  RelationGetRelationName(rel),
+									  get_namespace_name(relSchemaId)));
+				else if (checkobjtype == PUBLICATIONOBJ_TABLE)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add relation \"%s.%s\" to publication",
+								   get_namespace_name(relSchemaId),
+								   RelationGetRelationName(rel)),
+							errdetail("Table's schema \"%s\" is already part of the publication or part of the specified schema list.",
+									  get_namespace_name(relSchemaId)));
+			}
 		}
 	}
 }
@@ -297,7 +303,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 contain_invalid_rfcolumn(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+						 bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -324,7 +330,8 @@ contain_invalid_rfcolumn(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -697,18 +704,20 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
+
 	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
 	{
+		Assert(!relations);
+
 		/* Invalidate relcache so that publication info is rebuilt. */
 		CacheInvalidateRelcacheAll();
 	}
 	else
 	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
-
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
+		/* FOR [SKIP] ALL TABLES IN SCHEMA requires superuser */
 		if (list_length(schemaidlist) > 0 && !superuser())
 			ereport(ERROR,
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -728,16 +737,16 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 			PublicationAddTables(puboid, rels, true, NULL);
 			CloseTableList(rels);
 		}
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
@@ -906,7 +915,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		}
 
 		schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
-														PUBLICATION_PART_ALL);
+														PUBLICATION_PART_ALL,
+														false);
 		relids = list_concat_unique_oid(relids, schemarelids);
 
 		InvalidatePublicationRels(relids);
@@ -970,7 +980,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		 * Check if the relation is member of the existing schema in the
 		 * publication or member of the schema list specified.
 		 */
-		schemas = list_concat_copy(schemaidlist, GetPublicationSchemas(pubid));
+		schemas = list_concat(schemaidlist, GetPublicationSchemas(pubid));
 		CheckObjSchemaNotAlreadyInPublication(rels, schemas,
 											  PUBLICATIONOBJ_TABLE);
 
@@ -1124,7 +1134,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		List	   *delschemas = NIL;
 
 		/* Identify which schemas should be dropped */
-		delschemas = list_difference_oid(oldschemaids, schemaidlist);
+		delschemas = list_difference(oldschemaids, schemaidlist);
 
 		/*
 		 * Schema lock is held until the publication is altered to prevent
@@ -1152,6 +1162,20 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *tables, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+
+	bool		nonskipschema = false;
+	bool		skipschema = false;
+
+	foreach(lc, schemaidlist)
+	{
+		PublicationSchInfo *pub_sch = (PublicationSchInfo *) lfirst(lc);
+
+		if (!pub_sch->skip)
+			nonskipschema = true;
+		else
+			skipschema = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
@@ -1163,13 +1187,20 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 	 * Check that user is allowed to manipulate the publication tables in
 	 * schema
 	 */
-	if (schemaidlist && pubform->puballtables)
+	if (nonskipschema && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
+	if (skipschema && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				 errdetail("Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
+
 	/* Check that user is allowed to manipulate the publication tables. */
 	if (tables && pubform->puballtables)
 		ereport(ERROR,
@@ -1543,7 +1574,8 @@ LockSchemaList(List *schemalist)
 
 	foreach(lc, schemalist)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
+		Oid			schemaid = pubsch->oid;
 
 		/* Allow query cancel in case this takes a long time */
 		CHECK_FOR_INTERRUPTS();
@@ -1648,10 +1680,10 @@ PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists,
 
 	foreach(lc, schemas)
 	{
-		Oid			schemaid = lfirst_oid(lc);
 		ObjectAddress obj;
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
 
-		obj = publication_add_schema(pubid, schemaid, if_not_exists);
+		obj = publication_add_schema(pubid, pubsch, if_not_exists);
 		if (stmt)
 		{
 			EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress,
@@ -1675,7 +1707,8 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
 
 	foreach(lc, schemas)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
+		Oid			schemaid = pubsch->oid;
 
 		psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP,
 							   Anum_pg_publication_namespace_oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 80faae985e..e4ae136ac1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16385,7 +16385,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	if (stmt->objectType == OBJECT_TABLE)
 	{
 		ListCell   *lc;
-		List	   *schemaPubids = GetSchemaPublications(nspOid);
+		List	   *schemaPubids = GetSchemaPublications(nspOid, false);
 		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
 
 		foreach(lc, relPubids)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index d4f8455a2b..81ea805826 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4854,6 +4854,17 @@ _copyPublicationTable(const PublicationTable *from)
 	return newnode;
 }
 
+static PublicationSchInfo *
+_copyPublicationSchInfo(const PublicationSchInfo *from)
+{
+	PublicationSchInfo *newnode = makeNode(PublicationSchInfo);
+
+	COPY_SCALAR_FIELD(oid);
+	COPY_SCALAR_FIELD(skip);
+
+	return newnode;
+}
+
 static CreatePublicationStmt *
 _copyCreatePublicationStmt(const CreatePublicationStmt *from)
 {
@@ -5940,6 +5951,9 @@ copyObjectImpl(const void *from)
 		case T_PublicationObjSpec:
 			retval = _copyPublicationObject(from);
 			break;
+		case T_PublicationSchInfo:
+			retval = _copyPublicationSchInfo(from);
+			break;
 		case T_PublicationTable:
 			retval = _copyPublicationTable(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f1002afe7a..b2b911a94f 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -33,6 +33,7 @@
 #include "nodes/extensible.h"
 #include "nodes/pathnodes.h"
 #include "utils/datum.h"
+#include "catalog/pg_publication.h"
 
 
 /*
@@ -2326,6 +2327,16 @@ _equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
 	return true;
 }
 
+static bool
+_equalPublicationSchema(const PublicationSchInfo *a,
+						const PublicationSchInfo *b)
+{
+	COMPARE_SCALAR_FIELD(oid);
+	COMPARE_SCALAR_FIELD(skip);
+
+	return true;
+}
+
 static bool
 _equalCreatePublicationStmt(const CreatePublicationStmt *a,
 							const CreatePublicationStmt *b)
@@ -3935,6 +3946,9 @@ equal(const void *a, const void *b)
 		case T_PublicationObjSpec:
 			retval = _equalPublicationObject(a, b);
 			break;
+		case T_PublicationSchInfo:
+			retval = _equalPublicationSchema(a, b);
+			break;
 		case T_PublicationTable:
 			retval = _equalPublicationTable(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0036c2f9e2..239013af91 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -219,6 +219,10 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
+static void preprocess_alltables_pubobj_list(List *pubobjspec_list,
+											 core_yyscan_t yyscanner);
+static void check_skip_in_pubobj_list(List *pubobjspec_list,
+											 core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %}
@@ -446,7 +450,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list skip_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -9718,12 +9722,15 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *)n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES skip_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
 					n->for_all_tables = true;
+					n->pubobjects = (List *)$7;
+					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					preprocess_alltables_pubobj_list(n->pubobjects, yyscanner);
 					$$ = (Node *)n;
 				}
 			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
@@ -9733,6 +9740,7 @@ CreatePublicationStmt:
 					n->options = $6;
 					n->pubobjects = (List *)$5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_skip_in_pubobj_list(n->pubobjects, yyscanner);
 					$$ = (Node *)n;
 				}
 		;
@@ -9764,14 +9772,31 @@ PublicationObjSpec:
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
 					$$->name = $5;
+					$$->skip = false;
 					$$->location = @5;
 				}
 			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->skip = false;
 					$$->location = @5;
 				}
+			| SKIP ALL TABLES IN_P SCHEMA ColId
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
+					$$->name = $6;
+					$$->skip = true;
+					$$->location = @6;
+				}
+			| SKIP ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->skip = true;
+					$$->location = @6;
+				}
 			| ColId OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -9826,6 +9851,12 @@ pub_obj_list: 	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ skip_pub_obj_list:	pub_obj_list
+						{ $$ = $1; }
+					| /*EMPTY*/
+						{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -17448,6 +17479,7 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	ListCell   *cell;
 	PublicationObjSpec *pubobj;
 	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_CONTINUATION;
+	bool prevskipobj = false;
 
 	if (!pubobjspec_list)
 		return;
@@ -17465,7 +17497,10 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_CONTINUATION)
+		{
 			pubobj->pubobjtype = prevobjtype;
+			pubobj->skip = prevskipobj;
+		}
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
 		{
@@ -17513,6 +17548,64 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		}
 
 		prevobjtype = pubobj->pubobjtype;
+		prevskipobj = pubobj->skip;
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if any other option other that
+ * "SKIP ALL TABLES IN SCHEMA" is specified with "ALL TABLES" and throw an
+ * error.
+ */
+static void
+preprocess_alltables_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
+			!pubobj->skip)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("only SKIP ALL TABLES IN SCHEMA can be specified with ALL TABLES option"),
+					parser_errposition(pubobj->location));
+		}
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if "SKIP ALL TABLES IN SCHEMA" is specified
+ * with "ALL TABLES" and throw an error.
+ */
+static void
+check_skip_in_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA &&
+			pubobj->skip)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option"),
+					parser_errposition(pubobj->location));
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 5fddab3a3d..c1564394a8 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1745,7 +1745,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		 * the cache entry using a historic snapshot and all the later changes
 		 * are absorbed while decoding WAL.
 		 */
-		List	   *schemaPubids = GetSchemaPublications(schemaId);
+		List       *schemaPubids = GetSchemaPublications(schemaId, false);
+		List       *skipSchemaPubids = GetSchemaPublications(schemaId, true);
 		ListCell   *lc;
 		Oid			publish_as_relid = relid;
 		int			publish_ancestor_level = 0;
@@ -1824,22 +1825,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid	pub_relid = relid;
 			int	ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition root
-			 * and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -1858,7 +1843,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -1873,6 +1859,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables && !list_member_oid(skipSchemaPubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -1942,6 +1929,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(skipSchemaPubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce572..5771906589 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5538,6 +5538,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *skipschemapuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5569,7 +5571,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	/* Fetch the publication membership info. */
 	puboids = GetRelationPublications(relid);
 	schemaid = RelationGetNamespace(relation);
-	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid, false));
+	skipschemapuboids = GetSchemaPublications(schemaid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5584,11 +5587,21 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 											 GetRelationPublications(ancestor));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
-											 GetSchemaPublications(schemaid));
+											 GetSchemaPublications(schemaid, false));
+			skipschemapuboids = list_concat_unique_oid(skipschemapuboids,
+													   GetSchemaPublications(schemaid, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+
+	/*
+	 * Append "ALL TABLES" publications which does not exclude this
+	 * relation's schema.
+	 */
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 skipschemapuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5617,7 +5630,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			contain_invalid_rfcolumn(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+									 pubform->pubviaroot,
+									 pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e5816c4cce..bfe3d8c1ea 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4013,6 +4013,7 @@ getPublicationNamespaces(Archive *fout)
 	int			i_oid;
 	int			i_pnpubid;
 	int			i_pnnspid;
+	int			i_pnskip;
 	int			i,
 				j,
 				ntups;
@@ -4024,7 +4025,7 @@ getPublicationNamespaces(Archive *fout)
 
 	/* Collect all publication membership info. */
 	appendPQExpBufferStr(query,
-						 "SELECT tableoid, oid, pnpubid, pnnspid "
+						 "SELECT tableoid, oid, pnpubid, pnnspid, pnskip "
 						 "FROM pg_catalog.pg_publication_namespace");
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
@@ -4034,6 +4035,7 @@ getPublicationNamespaces(Archive *fout)
 	i_oid = PQfnumber(res, "oid");
 	i_pnpubid = PQfnumber(res, "pnpubid");
 	i_pnnspid = PQfnumber(res, "pnnspid");
+	i_pnskip = PQfnumber(res, "pnskip");
 
 	/* this allocation may be more than we need */
 	pubsinfo = pg_malloc(ntups * sizeof(PublicationSchemaInfo));
@@ -4043,6 +4045,7 @@ getPublicationNamespaces(Archive *fout)
 	{
 		Oid			pnpubid = atooid(PQgetvalue(res, i, i_pnpubid));
 		Oid			pnnspid = atooid(PQgetvalue(res, i, i_pnnspid));
+		char	   *pnskip = pg_strdup(PQgetvalue(res, i, i_pnskip));
 		PublicationInfo *pubinfo;
 		NamespaceInfo *nspinfo;
 
@@ -4065,7 +4068,10 @@ getPublicationNamespaces(Archive *fout)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubsinfo[j].dobj.objType = DO_PUBLICATION_TABLE_IN_SCHEMA;
+		if (strcmp(pnskip, "t") == 0)
+			pubsinfo[j].dobj.objType = DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA;
+		else
+			pubsinfo[j].dobj.objType = DO_PUBLICATION_TABLE_IN_SCHEMA;
 		pubsinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4190,13 +4196,15 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
  *	  dump the definition of the given publication schema mapping.
  */
 static void
-dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
+dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo,
+						 bool bskip)
 {
 	DumpOptions *dopt = fout->dopt;
 	NamespaceInfo *schemainfo = pubsinfo->pubschema;
 	PublicationInfo *pubinfo = pubsinfo->publication;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description = (bskip) ? "PUBLICATION SKIP TABLES IN SCHEMA" : "PUBLICATION TABLES IN SCHEMA";
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4206,8 +4214,12 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ", fmtId(pubinfo->dobj.name));
-	appendPQExpBuffer(query, "ADD ALL TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ", fmtId(pubinfo->dobj.name));
+
+	if (bskip)
+		appendPQExpBufferStr(query, "SKIP ");
+
+	appendPQExpBuffer(query, "ALL TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
 
 	/*
 	 * There is no point in creating drop query as the drop is done by schema
@@ -4218,7 +4230,7 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = schemainfo->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLES IN SCHEMA",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9853,9 +9865,15 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
+		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
+			dumpPublicationNamespace(fout,
+									 (const PublicationSchemaInfo *) dobj,
+									 true);
+			break;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
-									 (const PublicationSchemaInfo *) dobj);
+									 (const PublicationSchemaInfo *) dobj,
+									 false);
 			break;
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
@@ -17786,6 +17804,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
+			case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
 				/* Post-data objects: must come after the post-data boundary */
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 772dc0cf7a..2d39975228 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_POLICY,
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
+	DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
 } DumpableObjectType;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 1592090839..d024936b14 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -91,6 +91,7 @@ enum dbObjectTypePriorities
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
 	PRIO_PUBLICATION_REL,
+	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
 	PRIO_DEFAULT_ACL,			/* done in ACL pass */
@@ -145,6 +146,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
+	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
 };
@@ -1488,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION TABLE (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
+			snprintf(buf, bufsize,
+					 "PUBLICATION SKIP TABLES IN SCHEMA (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLES IN SCHEMA (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fd1052e5db..c7b164191e 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2392,6 +2392,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2474,6 +2483,27 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA dump_test' => {
+		create_order => 51,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA dump_test;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA dump_test;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA public' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA public;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA public;\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 714097cad1..9cba84d694 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2896,6 +2896,7 @@ describeOneTableDetails(const char *schemaname,
 								  "		JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "		JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "  AND pn.pnskip = 'f'\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, pg_get_expr(pr.prqual, c.oid)\n"
@@ -6066,6 +6067,7 @@ describePublications(const char *pattern)
 								  "FROM pg_catalog.pg_namespace n\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
+								  "  AND pn.pnskip = 'f'\n"
 								  "ORDER BY 1", pubid);
 				if (!addFooterToPublicationDesc(&buf, "Tables from schemas:",
 												true, &cont))
@@ -6073,6 +6075,21 @@ describePublications(const char *pattern)
 			}
 		}
 
+		if (pset.sversion >= 150000)
+		{
+			/* Get the skip schemas for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT n.nspname\n"
+							  "FROM pg_catalog.pg_namespace n\n"
+							  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
+							  "WHERE pn.pnpubid = '%s'\n"
+							  "  AND pn.pnskip = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Skip tables from schemas:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5c064595a9..cdcc33dfb7 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1814,8 +1814,10 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* 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|DROP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
@@ -1836,13 +1838,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",", "WHERE (");
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH(",");
-	/* ALTER PUBLICATION <name> DROP */
-	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
-	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
@@ -2973,7 +2975,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
@@ -2995,11 +2997,14 @@ psql_completion(const char *text, int start, int end)
 	 * Complete "CREATE PUBLICATION <name> FOR ALL TABLES IN SCHEMA <schema>,
 	 * ..."
 	 */
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA") ||
+			 Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) && (!ends_with(prev_wd, ',')))
+	else if ((Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) ||
+			  Matches("CREATE", "PUBLICATION", MatchAny, "SKIP", "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny)) &&
+			 (!ends_with(prev_wd, ',')))
 		COMPLETE_WITH("WITH (");
 	/* Complete "CREATE PUBLICATION <name> [...] WITH" */
 	else if (HeadMatches("CREATE", "PUBLICATION") && TailMatches("WITH", "("))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index fe773cf9b7..206def7e30 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -102,6 +102,13 @@ typedef struct PublicationRelInfo
 	Node	   *whereClause;
 } PublicationRelInfo;
 
+typedef struct PublicationSchInfo
+{
+	NodeTag		type;
+	Oid			oid;
+	bool		skip;
+} PublicationSchInfo;
+
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
@@ -124,24 +131,27 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
-extern List *GetSchemaPublications(Oid schemaid);
+extern List *GetSchemaPublications(Oid schemaid, bool skippub);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
 										   PublicationPartOpt pub_partopt);
 extern List *GetAllSchemaPublicationRelations(Oid puboid,
-											  PublicationPartOpt pub_partopt);
+											  PublicationPartOpt pub_partopt,
+											  bool bskip);
 extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level,
+											bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
-extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
+extern ObjectAddress publication_add_schema(Oid pubid,
+											PublicationSchInfo *pubsch,
 											bool if_not_exists);
 
 extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
diff --git a/src/include/catalog/pg_publication_namespace.h b/src/include/catalog/pg_publication_namespace.h
index e4306da02e..1e1b82cd65 100644
--- a/src/include/catalog/pg_publication_namespace.h
+++ b/src/include/catalog/pg_publication_namespace.h
@@ -32,6 +32,7 @@ CATALOG(pg_publication_namespace,8901,PublicationNamespaceRelationId)
 	Oid			oid;			/* oid */
 	Oid			pnpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			pnnspid BKI_LOOKUP(pg_namespace);	/* Oid of the schema */
+	bool		pnskip BKI_DEFAULT(f);
 } FormData_pg_publication_namespace;
 
 /* ----------------
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 7813cbcb6b..0f27cd8347 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,6 +32,7 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool contain_invalid_rfcolumn(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+									 List *ancestors, bool pubviaroot,
+									 bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 5d075f0c34..18bba3bf3b 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -490,6 +490,7 @@ typedef enum NodeTag
 	T_PartitionCmd,
 	T_VacuumRelation,
 	T_PublicationObjSpec,
+	T_PublicationSchInfo,
 	T_PublicationTable,
 
 	/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 6f83a79a96..1d4cf015fe 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3672,6 +3672,7 @@ typedef struct PublicationObjSpec
 	PublicationObjSpecType pubobjtype;	/* type of this publication object */
 	char	   *name;
 	PublicationTable *pubtable;
+	bool		skip;
 	int			location;		/* token location, or -1 if unknown */
 } PublicationObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4e191c120a..faa2ce34e0 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -81,6 +81,35 @@ DETAIL:  Tables from schema cannot be added to, dropped from, or set on FOR ALL
 ALTER PUBLICATION testpub_foralltables SET ALL TABLES IN SCHEMA pub_test;
 ERROR:  publication "testpub_foralltables" is defined as FOR ALL TABLES
 DETAIL:  Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.
+-- should be able to add skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Skip tables from schemas:
+    "pub_test"
+
+-- should be able to drop skip schema from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+(1 row)
+
+-- should be able to set skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Skip tables from schemas:
+    "pub_test"
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -116,6 +145,18 @@ ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 Tables from schemas:
     "pub_test"
 
+-- fail - can't add skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip schema from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -141,6 +182,18 @@ ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 Tables:
     "pub_test.testpub_nopk"
 
+-- fail - can't add skip schema to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip schema from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip schema to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -163,10 +216,37 @@ Publications:
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
 --------------------------+------------+---------+---------+---------+-----------+----------
  regress_publication_user | t          | t       | t       | f       | f         | f
-(1 row)
+Skip tables from schemas:
+    "pub_test"
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skipschema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA pub_test;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_skipschema
+                        Publication testpub_foralltables_skipschema
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Skip tables from schemas:
+    "pub_test"
 
+-- fail - can't specify skip schema along with table publication
+CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...E pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
+-- fail - can't specify skip schema along with schema publication
+CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...BLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
+-- fail - can't specify only skip schema while create publication
+CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...N testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 5457c56b33..de1970f9e0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -58,6 +58,16 @@ ALTER PUBLICATION testpub_foralltables DROP ALL TABLES IN SCHEMA pub_test;
 -- fail - can't set schema to 'FOR ALL TABLES' publication
 ALTER PUBLICATION testpub_foralltables SET ALL TABLES IN SCHEMA pub_test;
 
+-- should be able to add skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+-- should be able to drop skip schema from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+-- should be able to set skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -71,6 +81,13 @@ ALTER PUBLICATION testpub_fortable DROP ALL TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 \dRp+ testpub_fortable
 
+-- fail - can't add skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't drop skip schema from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't set skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -85,12 +102,34 @@ ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk;
 ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
 
+-- fail - can't add skip schema to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't drop skip schema from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't set skip schema to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
+
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skipschema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA pub_test;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_skipschema
+
+-- fail - can't specify skip schema along with table publication
+CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+
+-- fail - can't specify skip schema along with schema publication
+CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+
+-- fail - can't specify only skip schema while create publication
+CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/030_rep_changes_skip_schema.pl b/src/test/subscription/t/030_rep_changes_skip_schema.pl
new file mode 100644
index 0000000000..7a16ad350c
--- /dev/null
+++ b/src/test/subscription/t/030_rep_changes_skip_schema.pl
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for skip schema publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES SKIP ALL TABLES IN SCHEMA
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA sch1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the schema table data does not sync for skip schemas
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is skipped for skip schemas');
+
+# Insert some data into few tables and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to skip data changes in public and verify that subscriber does not get
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add SKIP ALL TABLES IN SCHEMA public");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop skip schema public and verify that subscriber gets
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop SKIP ALL TABLES IN SCHEMA public");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 93d5190508..e8569f2835 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2065,6 +2065,7 @@ PublicationObjSpecType
 PublicationPartOpt
 PublicationRelInfo
 PublicationSchemaInfo
+PublicationSchInfo
 PublicationTable
 PullFilter
 PullFilterOps
-- 
2.32.0

#2vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#1)
1 attachment(s)
Re: Skipping schema changes in publication

On Tue, Mar 22, 2022 at 12:38 PM vignesh C <vignesh21@gmail.com> wrote:

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.

The patch was not applying on top of HEAD because of the recent
commits, attached patch is rebased on top of HEAD.

Regards,
Vignesh

Attachments:

v1-0001-Skip-publishing-the-tables-of-schema.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Skip-publishing-the-tables-of-schema.patchDownload
From 1524254629c72149ae99d13aae283d3aa6a70253 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 26 Mar 2022 19:19:31 +0530
Subject: [PATCH v1] Skip publishing the tables of schema.

A new option "SKIP ALL TABLES IN SCHEMA" in Create/Alter Publication allows
one or more skip schemas to be specified, publisher will skip sending the data
of the tables present in the skip schema to the subscriber.

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to maintain
the schemas that the user wants to skip publishing through the publication.
Modified the output plugin (pgoutput) to skip publishing the changes if the
relation is part of skip schema publication.

Updates pg_dump to identify and dump skip schema publications. Updates the \d
family of commands to display skip schema publications and \dRp+ variant will
now display associated skip schemas if any.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   7 +-
 doc/src/sgml/ref/alter_publication.sgml       |  28 ++-
 doc/src/sgml/ref/create_publication.sgml      |  28 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  70 +++++--
 src/backend/commands/publicationcmds.c        | 186 +++++++++++-------
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/nodes/copyfuncs.c                 |  14 ++
 src/backend/nodes/equalfuncs.c                |  14 ++
 src/backend/parser/gram.y                     | 119 ++++++++++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  21 +-
 src/bin/pg_dump/pg_dump.c                     |  33 +++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 +++
 src/bin/psql/describe.c                       |  19 +-
 src/bin/psql/tab-complete.c                   |  26 ++-
 src/include/catalog/pg_publication.h          |  20 +-
 .../catalog/pg_publication_namespace.h        |   1 +
 src/include/commands/publicationcmds.h        |   6 +-
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/test/regress/expected/publication.out     |  81 +++++++-
 src/test/regress/sql/publication.sql          |  41 +++-
 .../t/032_rep_changes_skip_schema.pl          |  96 +++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 28 files changed, 742 insertions(+), 154 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_skip_schema.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 560e205b95..59b0126c18 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6304,6 +6304,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        A null value indicates that all columns are published.
       </para></entry>
      </row>
+
+    <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pnskip</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the schema is skip schema
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 555fbd749c..e2a4b89226 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -599,9 +599,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
 
   <para>
    To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   table. To add all tables in schema or skip all tables in schema to a
+   publication, the user must be a superuser. To create a publication that
+   publishes all tables or all tables in schema automatically, the user must be
+   a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 40366a10fe..c854722bda 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     SEQUENCE <replaceable class="parameter">sequence_name</replaceable> [, ... ]
-    ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+    [SKIP] ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
     ALL SEQUENCES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -73,12 +73,12 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
+   The <literal>ADD [SKIP] ALL TABLES IN SCHEMA</literal> and
+   <literal>SET [SKIP] ALL TABLES IN SCHEMA</literal> to a publication requires
+   the invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
    <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
+   of a <literal>FOR ALL TABLES</literal> or <literal>FOR [SKIP] ALL TABLES IN
    SCHEMA</literal> publication must be a superuser. However, a superuser can
    change the ownership of a publication regardless of these restrictions.
   </para>
@@ -90,6 +90,14 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    adding/setting a table to a publication that already has a table's schema as
    part of the specified schema is not supported.
   </para>
+
+  <para>
+   The <literal>ADD SKIP ALL TABLES IN SCHEMA</literal> and
+   <literal>SET SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+   <literal>FOR ALL TABLES</literal> publication. It is not supported for
+   <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+   <literal>FOR TABLE</literal> publication.
+  </para>
  </refsect1>
 
  <refsect1>
@@ -220,6 +228,16 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
 </programlisting>
   </para>
+
+   <para>
+   Add skip schemas <structname>sales_june</structname> and
+   <structname>sales_july</structname> to the publication
+   <structname>mypublication</structname>:
+<programlisting>
+ALTER PUBLICATION mypublication ADD SKIP ALL TABLES IN SCHEMA sales_june, sales_july;
+</programlisting>
+  </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index d2739968d9..e83864d175 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">object type</replaceable> is one of:</phrase>
 
-    TABLES
+    TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA }]
     SEQUENCES
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
@@ -146,6 +146,23 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>SKIP ALL TABLES IN SCHEMA</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that skips replicating changes for all
+      tables in the specified list of schemas.
+     </para>
+
+     <para>
+      <literal>SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>FOR ALL TABLES IN SCHEMA</literal></term>
     <term><literal>FOR ALL SEQUENCES IN SCHEMA</literal></term>
@@ -355,6 +372,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
    <structname>sales</structname>:
 <programlisting>
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of all the tables present in the schema
+   <structname>marketing</structname> and <structname>sales</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE SKIP ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index caabb06c53..4ba4140933 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1856,8 +1856,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        schemas and the skip schema associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a5a54e676e..8a6af83ef9 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -338,7 +338,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -352,6 +353,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		Oid			ancestor = lfirst_oid(lc);
 		List	   *apubids = GetRelationPublications(ancestor);
 		List	   *aschemaPubids = NIL;
+		List       *askipschemaPubids = NIL;
 
 		level++;
 
@@ -366,8 +368,11 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		{
 			/* we only search for ancestors of tables, so PUB_OBJTYPE_TABLE */
 			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor),
-												  PUB_OBJTYPE_TABLE);
-			if (list_member_oid(aschemaPubids, puboid))
+												  PUB_OBJTYPE_TABLE, false);
+			askipschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor),
+													  PUB_OBJTYPE_TABLE, true);
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(askipschemaPubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -631,13 +636,14 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Insert new publication / schema mapping.
  */
 ObjectAddress
-publication_add_schema(Oid pubid, Oid schemaid, char objectType, bool if_not_exists)
+publication_add_schema(Oid pubid, PublicationSchInfo *pubsch, char objectType, bool if_not_exists)
 {
 	Relation	rel;
 	HeapTuple	tup;
 	Datum		values[Natts_pg_publication_namespace];
 	bool		nulls[Natts_pg_publication_namespace];
 	Oid			psschid;
+	Oid			schemaid = pubsch->oid;
 	Publication *pub = GetPublication(pubid);
 	List	   *schemaRels = NIL;
 	ObjectAddress myself,
@@ -683,6 +689,8 @@ publication_add_schema(Oid pubid, Oid schemaid, char objectType, bool if_not_exi
 		ObjectIdGetDatum(schemaid);
 	values[Anum_pg_publication_namespace_pntype - 1] =
 		CharGetDatum(objectType);
+	values[Anum_pg_publication_namespace_pnskip - 1] =
+		BoolGetDatum(pubsch->skip);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -890,13 +898,23 @@ GetAllSequencesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *skipschemaidlist = NIL;
+	List	   *pubschemalist = GetPublicationSchemas(pubid, PUB_OBJTYPE_TABLE);
+	ListCell   *cell;
+
+	foreach(cell, pubschemalist)
+	{
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
+
+		skipschemaidlist = lappend_oid(result, pubsch->oid);
+	}
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -911,9 +929,11 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	{
 		Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 		Oid			relid = relForm->oid;
+		Oid			schid = get_rel_namespace(relid);
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(skipschemaidlist, schid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -932,9 +952,11 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		{
 			Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 			Oid			relid = relForm->oid;
+			Oid			schid = get_rel_namespace(relid);
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(skipschemaidlist, schid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -983,10 +1005,14 @@ GetPublicationSchemas(Oid pubid, char objectType)
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_namespace pubsch;
+		PublicationSchInfo *schinfo = makeNode(PublicationSchInfo);
+
 
 		pubsch = (Form_pg_publication_namespace) GETSTRUCT(tup);
+		schinfo->oid = pubsch->pnnspid;
+		schinfo->skip = pubsch->pnskip;
 
-		result = lappend_oid(result, pubsch->pnnspid);
+		result = lappend(result, schinfo);
 	}
 
 	systable_endscan(scan);
@@ -1005,7 +1031,7 @@ GetPublicationSchemas(Oid pubid, char objectType)
  * Which is why we handle the PUB_OBJTYPE_UNSUPPORTED object type too.
  */
 List *
-GetSchemaPublications(Oid schemaid, char objectType)
+GetSchemaPublications(Oid schemaid, char objectType, bool skippub)
 {
 	List	   *result = NIL;
 	CatCList   *pubschlist;
@@ -1030,7 +1056,8 @@ GetSchemaPublications(Oid schemaid, char objectType)
 		if (pntype != objectType)
 			continue;
 
-		result = lappend_oid(result, pubid);
+		if (skippub == ((Form_pg_publication_namespace) GETSTRUCT(tup))->pnskip)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubschlist);
@@ -1116,7 +1143,7 @@ GetSchemaPublicationRelations(Oid schemaid, char objectType,
  */
 List *
 GetAllSchemaPublicationRelations(Oid pubid, char objectType,
-								 PublicationPartOpt pub_partopt)
+								 PublicationPartOpt pub_partopt, bool bskip)
 {
 	List	   *result = NIL;
 	List	   *pubschemalist = GetPublicationSchemas(pubid, objectType);
@@ -1126,12 +1153,16 @@ GetAllSchemaPublicationRelations(Oid pubid, char objectType,
 
 	foreach(cell, pubschemalist)
 	{
-		Oid			schemaid = lfirst_oid(cell);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
 		List	   *schemaRels = NIL;
 
-		schemaRels = GetSchemaPublicationRelations(schemaid, objectType,
-												   pub_partopt);
-		result = list_concat(result, schemaRels);
+		/* Skip the skip schemas if bskip is true */
+		if (bskip && !pubsch->skip)
+		{
+			schemaRels = GetSchemaPublicationRelations(pubsch->oid, objectType,
+													   pub_partopt);
+			result = list_concat(result, schemaRels);
+		}
 	}
 
 	return result;
@@ -1303,7 +1334,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
@@ -1319,7 +1351,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 															PUB_OBJTYPE_TABLE,
 															publication->pubviaroot ?
 															PUBLICATION_PART_ROOT :
-															PUBLICATION_PART_LEAF);
+															PUBLICATION_PART_LEAF,
+															true);
 			tables = list_concat_unique_oid(relids, schemarelids);
 
 			/*
@@ -1398,7 +1431,8 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 															PUB_OBJTYPE_SEQUENCE,
 															publication->pubviaroot ?
 															PUBLICATION_PART_ROOT :
-															PUBLICATION_PART_LEAF);
+															PUBLICATION_PART_LEAF,
+															true);
 			sequences = list_concat_unique_oid(relids, schemarelids);
 		}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 84e37df783..cbc805bbdd 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -185,8 +185,8 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 
 	foreach(cell, pubobjspec_list)
 	{
-		Oid			schemaid;
 		List	   *search_path;
+		PublicationSchInfo *pubsch = makeNode(PublicationSchInfo);
 
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
@@ -199,16 +199,18 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 				*sequences = lappend(*sequences, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
-				schemaid = get_namespace_oid(pubobj->name, false);
+				pubsch->oid = get_namespace_oid(pubobj->name, false);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*tables_schemas = list_append_unique_oid(*tables_schemas, schemaid);
+				*tables_schemas = list_append_unique(*tables_schemas, pubsch);
 				break;
 			case PUBLICATIONOBJ_SEQUENCES_IN_SCHEMA:
-				schemaid = get_namespace_oid(pubobj->name, false);
+				pubsch->oid = get_namespace_oid(pubobj->name, false);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*sequences_schemas = list_append_unique_oid(*sequences_schemas, schemaid);
+				*sequences_schemas = list_append_unique(*sequences_schemas, pubsch);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA:
 				search_path = fetch_search_path(false);
@@ -217,11 +219,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 							errcode(ERRCODE_UNDEFINED_SCHEMA),
 							errmsg("no schema has been selected for CURRENT_SCHEMA"));
 
-				schemaid = linitial_oid(search_path);
+				pubsch->oid = linitial_oid(search_path);
 				list_free(search_path);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*tables_schemas = list_append_unique_oid(*tables_schemas, schemaid);
+				*tables_schemas = list_append_unique(*tables_schemas, pubsch);
 				break;
 			case PUBLICATIONOBJ_SEQUENCES_IN_CUR_SCHEMA:
 				search_path = fetch_search_path(false);
@@ -230,11 +233,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 							errcode(ERRCODE_UNDEFINED_SCHEMA),
 							errmsg("no schema has been selected for CURRENT_SCHEMA"));
 
-				schemaid = linitial_oid(search_path);
+				pubsch->oid = linitial_oid(search_path);
 				list_free(search_path);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*sequences_schemas = list_append_unique_oid(*sequences_schemas, schemaid);
+				*sequences_schemas = list_append_unique(*sequences_schemas, pubsch);
 				break;
 			default:
 				/* shouldn't happen */
@@ -260,40 +264,45 @@ CheckObjSchemaNotAlreadyInPublication(List *rels, List *schemaidlist,
 		Relation	rel = pub_rel->relation;
 		Oid			relSchemaId = RelationGetNamespace(rel);
 
-		if (list_member_oid(schemaidlist, relSchemaId))
+		foreach(lc, schemaidlist)
 		{
-			if (checkobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add schema \"%s\" to publication",
-							   get_namespace_name(relSchemaId)),
-						errdetail("Table \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
-								  RelationGetRelationName(rel),
-								  get_namespace_name(relSchemaId)));
-			else if (checkobjtype == PUBLICATIONOBJ_SEQUENCES_IN_SCHEMA)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add schema \"%s\" to publication",
-							   get_namespace_name(relSchemaId)),
-						errdetail("Sequence \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
-								  RelationGetRelationName(rel),
-								  get_namespace_name(relSchemaId)));
-			else if (checkobjtype == PUBLICATIONOBJ_TABLE)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add relation \"%s.%s\" to publication",
-							   get_namespace_name(relSchemaId),
-							   RelationGetRelationName(rel)),
-						errdetail("Table's schema \"%s\" is already part of the publication or part of the specified schema list.",
-								  get_namespace_name(relSchemaId)));
-			else if (checkobjtype == PUBLICATIONOBJ_SEQUENCE)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add relation \"%s.%s\" to publication",
-							   get_namespace_name(relSchemaId),
-							   RelationGetRelationName(rel)),
-						errdetail("Sequence's schema \"%s\" is already part of the publication or part of the specified schema list.",
-								  get_namespace_name(relSchemaId)));
+			PublicationSchInfo *pub_sch = (PublicationSchInfo *) lfirst(lc);
+
+			if (pub_sch->oid == relSchemaId)
+			{
+				if (checkobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add schema \"%s\" to publication",
+								get_namespace_name(relSchemaId)),
+							errdetail("Table \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
+									RelationGetRelationName(rel),
+									get_namespace_name(relSchemaId)));
+				else if (checkobjtype == PUBLICATIONOBJ_SEQUENCES_IN_SCHEMA)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add schema \"%s\" to publication",
+								get_namespace_name(relSchemaId)),
+							errdetail("Sequence \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
+									RelationGetRelationName(rel),
+									get_namespace_name(relSchemaId)));
+				else if (checkobjtype == PUBLICATIONOBJ_TABLE)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add relation \"%s.%s\" to publication",
+								get_namespace_name(relSchemaId),
+								RelationGetRelationName(rel)),
+							errdetail("Table's schema \"%s\" is already part of the publication or part of the specified schema list.",
+									get_namespace_name(relSchemaId)));
+				else if (checkobjtype == PUBLICATIONOBJ_SEQUENCE)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add relation \"%s.%s\" to publication",
+								get_namespace_name(relSchemaId),
+								RelationGetRelationName(rel)),
+							errdetail("Sequence's schema \"%s\" is already part of the publication or part of the specified schema list.",
+									get_namespace_name(relSchemaId)));
+			}
 		}
 	}
 }
@@ -343,7 +352,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+						 bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -370,7 +379,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -419,7 +429,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+						 bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -438,7 +448,7 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -907,9 +917,14 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &tables, &sequences,
+							   &tables_schemaidlist, &sequences_schemaidlist);
+
 	/* Associate objects with the publication. */
 	if (for_all_tables || for_all_sequences)
 	{
+		Assert(!tables);
+
 		/* Invalidate relcache so that publication info is rebuilt. */
 		CacheInvalidateRelcacheAll();
 	}
@@ -920,12 +935,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	 */
 	if (!for_all_tables || !for_all_sequences)
 	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate,
-								   &tables, &sequences,
-								   &tables_schemaidlist,
-								   &sequences_schemaidlist);
-
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
+		/* FOR [SKIP] ALL TABLES IN SCHEMA requires superuser */
 		if (list_length(tables_schemaidlist) > 0 && !superuser())
 			ereport(ERROR,
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -968,19 +978,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 			CloseRelationList(rels);
 		}
 
-		/* tables added through a schema */
-		if (list_length(tables_schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(tables_schemaidlist);
-			PublicationAddSchemas(puboid,
-								  tables_schemaidlist, PUB_OBJTYPE_TABLE,
-								  true, NULL);
-		}
-
 		/* sequences added through a schema */
 		if (list_length(sequences_schemaidlist) > 0)
 		{
@@ -995,6 +992,19 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 		}
 	}
 
+	/* tables added through a schema */
+	if (list_length(tables_schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(tables_schemaidlist);
+		PublicationAddSchemas(puboid,
+								tables_schemaidlist, PUB_OBJTYPE_TABLE,
+								true, NULL);
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
@@ -1190,7 +1200,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		/* tables */
 		schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
 														PUB_OBJTYPE_TABLE,
-														PUBLICATION_PART_ALL);
+														PUBLICATION_PART_ALL,
+														false);
 		relids = list_concat_unique_oid(relids, schemarelids);
 
 		/* sequences */
@@ -1201,7 +1212,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 
 		schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
 														PUB_OBJTYPE_SEQUENCE,
-														PUBLICATION_PART_ALL);
+														PUBLICATION_PART_ALL,
+														false);
 		relids = list_concat_unique_oid(relids, schemarelids);
 
 		InvalidatePublicationRels(relids);
@@ -1265,9 +1277,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		 * Check if the relation is member of the existing schema in the
 		 * publication or member of the schema list specified.
 		 */
-		schemas = list_concat_copy(schemaidlist,
-								   GetPublicationSchemas(pubid,
-														 PUB_OBJTYPE_TABLE));
+		schemas = list_concat(schemaidlist,
+							  GetPublicationSchemas(pubid, PUB_OBJTYPE_TABLE));
 		CheckObjSchemaNotAlreadyInPublication(rels, schemas,
 											  PUBLICATIONOBJ_TABLE);
 
@@ -1462,7 +1473,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		List	   *delschemas = NIL;
 
 		/* Identify which schemas should be dropped */
-		delschemas = list_difference_oid(oldschemaids, schemaidlist);
+		delschemas = list_difference(oldschemaids, schemaidlist);
 
 		/*
 		 * Schema lock is held until the publication is altered to prevent
@@ -1491,6 +1502,20 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *sequences, List *sequences_schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+
+	bool		nonskipschema = false;
+	bool		skipschema = false;
+
+	foreach(lc, tables_schemaidlist)
+	{
+		PublicationSchInfo *pub_sch = (PublicationSchInfo *) lfirst(lc);
+
+		if (!pub_sch->skip)
+			nonskipschema = true;
+		else
+			skipschema = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		(tables_schemaidlist || sequences_schemaidlist) && !superuser())
@@ -1502,13 +1527,20 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 	 * Check that user is allowed to manipulate the publication tables in
 	 * schema
 	 */
-	if (tables_schemaidlist && pubform->puballtables)
+	if (nonskipschema && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
+	if (skipschema && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				 errdetail("Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
+
 	/*
 	 * Check that user is allowed to manipulate the publication sequences in
 	 * schema
@@ -2054,7 +2086,8 @@ LockSchemaList(List *schemalist)
 
 	foreach(lc, schemalist)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
+		Oid			schemaid = pubsch->oid;
 
 		/* Allow query cancel in case this takes a long time */
 		CHECK_FOR_INTERRUPTS();
@@ -2164,10 +2197,10 @@ PublicationAddSchemas(Oid pubid, List *schemas, char objectType,
 
 	foreach(lc, schemas)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
 		ObjectAddress obj;
 
-		obj = publication_add_schema(pubid, schemaid, objectType, if_not_exists);
+		obj = publication_add_schema(pubid, pubsch, objectType, if_not_exists);
 		if (stmt)
 		{
 			EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress,
@@ -2191,7 +2224,8 @@ PublicationDropSchemas(Oid pubid, List *schemas, char objectType, bool missing_o
 
 	foreach(lc, schemas)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
+		Oid			schemaid = pubsch->oid;
 
 		psid = GetSysCacheOid3(PUBLICATIONNAMESPACEMAP,
 							   Anum_pg_publication_namespace_oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 124b9961dc..2b1e29be10 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16389,7 +16389,9 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	if (stmt->objectType == OBJECT_TABLE)
 	{
 		ListCell   *lc;
-		List	   *schemaPubids = GetSchemaPublications(nspOid, PUB_OBJTYPE_TABLE);
+		List	   *schemaPubids = GetSchemaPublications(nspOid,
+														 PUB_OBJTYPE_TABLE,
+														 false);
 		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
 
 		foreach(lc, relPubids)
@@ -16410,7 +16412,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	else if (stmt->objectType == OBJECT_SEQUENCE)
 	{
 		ListCell   *lc;
-		List	   *schemaPubids = GetSchemaPublications(nspOid, PUB_OBJTYPE_SEQUENCE);
+		List	   *schemaPubids = GetSchemaPublications(nspOid, PUB_OBJTYPE_SEQUENCE, false);
 		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
 
 		foreach(lc, relPubids)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index e38ff4000f..89c111e18c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4855,6 +4855,17 @@ _copyPublicationTable(const PublicationTable *from)
 	return newnode;
 }
 
+static PublicationSchInfo *
+_copyPublicationSchInfo(const PublicationSchInfo *from)
+{
+	PublicationSchInfo *newnode = makeNode(PublicationSchInfo);
+
+	COPY_SCALAR_FIELD(oid);
+	COPY_SCALAR_FIELD(skip);
+
+	return newnode;
+}
+
 static CreatePublicationStmt *
 _copyCreatePublicationStmt(const CreatePublicationStmt *from)
 {
@@ -5941,6 +5952,9 @@ copyObjectImpl(const void *from)
 		case T_PublicationObjSpec:
 			retval = _copyPublicationObject(from);
 			break;
+		case T_PublicationSchInfo:
+			retval = _copyPublicationSchInfo(from);
+			break;
 		case T_PublicationTable:
 			retval = _copyPublicationTable(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 0f330e3c70..11706e7816 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -33,6 +33,7 @@
 #include "nodes/extensible.h"
 #include "nodes/pathnodes.h"
 #include "utils/datum.h"
+#include "catalog/pg_publication.h"
 
 
 /*
@@ -2327,6 +2328,16 @@ _equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
 	return true;
 }
 
+static bool
+_equalPublicationSchema(const PublicationSchInfo *a,
+						const PublicationSchInfo *b)
+{
+	COMPARE_SCALAR_FIELD(oid);
+	COMPARE_SCALAR_FIELD(skip);
+
+	return true;
+}
+
 static bool
 _equalCreatePublicationStmt(const CreatePublicationStmt *a,
 							const CreatePublicationStmt *b)
@@ -3936,6 +3947,9 @@ equal(const void *a, const void *b)
 		case T_PublicationObjSpec:
 			retval = _equalPublicationObject(a, b);
 			break;
+		case T_PublicationSchInfo:
+			retval = _equalPublicationSchema(a, b);
+			break;
 		case T_PublicationTable:
 			retval = _equalPublicationTable(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 945a9ada8b..adddb33666 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -219,6 +219,12 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
+static void preprocess_alltables_pubobj_list(List *pubobjspec_list,
+											 List *for_all_objects,
+											 int location,
+											 core_yyscan_t yyscanner);
+static void check_skip_in_pubobj_list(List *pubobjspec_list,
+											 core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %}
@@ -447,6 +453,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_obj_type_list
+				skip_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -9716,12 +9723,18 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *)n;
 				}
-			| CREATE PUBLICATION name FOR ALL pub_obj_type_list opt_definition
+			| CREATE PUBLICATION name FOR ALL pub_obj_type_list skip_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
 					n->for_all_objects = $6;
+					n->pubobjects = (List *)$7;
+					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					preprocess_alltables_pubobj_list(n->pubobjects,
+													 n->for_all_objects,
+													 @6,
+													 yyscanner);
 					$$ = (Node *)n;
 				}
 			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
@@ -9731,6 +9744,7 @@ CreatePublicationStmt:
 					n->options = $6;
 					n->pubobjects = (List *)$5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_skip_in_pubobj_list(n->pubobjects, yyscanner);
 					$$ = (Node *)n;
 				}
 		;
@@ -9763,18 +9777,36 @@ PublicationObjSpec:
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
 					$$->name = $5;
+					$$->skip = false;
 					$$->location = @5;
 				}
 			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->skip = false;
 					$$->location = @5;
 				}
+			| SKIP ALL TABLES IN_P SCHEMA ColId
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
+					$$->name = $6;
+					$$->skip = true;
+					$$->location = @6;
+				}
+			| SKIP ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->skip = true;
+					$$->location = @6;
+				}
 			| SEQUENCE relation_expr
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_SEQUENCE;
+					$$->skip = false;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
 				}
@@ -9783,12 +9815,14 @@ PublicationObjSpec:
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_SEQUENCES_IN_SCHEMA;
 					$$->name = $5;
+					$$->skip = false;
 					$$->location = @5;
 				}
 			| ALL SEQUENCES IN_P SCHEMA CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_SEQUENCES_IN_CUR_SCHEMA;
+					$$->skip = false;
 					$$->location = @5;
 				}
 			| ColId opt_column_list OptWhereClause
@@ -9865,6 +9899,12 @@ pub_obj_type_list:	pub_obj_type
 	;
 
 
+ skip_pub_obj_list:	pub_obj_list
+						{ $$ = $1; }
+					| /*EMPTY*/
+						{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -17482,6 +17522,7 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	ListCell   *cell;
 	PublicationObjSpec *pubobj;
 	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_CONTINUATION;
+	bool prevskipobj = false;
 
 	if (!pubobjspec_list)
 		return;
@@ -17499,7 +17540,10 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_CONTINUATION)
+		{
 			pubobj->pubobjtype = prevobjtype;
+			pubobj->skip = prevskipobj;
+		}
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE ||
 			pubobj->pubobjtype == PUBLICATIONOBJ_SEQUENCE)
@@ -17579,6 +17623,77 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		}
 
 		prevobjtype = pubobj->pubobjtype;
+		prevskipobj = pubobj->skip;
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if any other option other that
+ * "SKIP ALL TABLES IN SCHEMA" is specified with "ALL TABLES" and throw an
+ * error.
+ */
+static void
+preprocess_alltables_pubobj_list(List *pubobjspec_list, List *for_all_objects,
+								 int location, core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+	bool for_all_tables = false;
+
+	if (!pubobjspec_list)
+		return;
+
+	/* Translate the list of object types (represented by strings) to bool flags. */
+	foreach (cell, for_all_objects)
+	{
+		char   *val = strVal(lfirst(cell));
+		if (strcmp(val, "tables") == 0)
+			for_all_tables = true;
+	}
+
+	if (!for_all_tables)
+		ereport(ERROR,
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option"),
+				parser_errposition(location));
+
+	foreach(cell, pubobjspec_list)
+	{
+		PublicationObjSpec *pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
+			!pubobj->skip)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("only SKIP ALL TABLES IN SCHEMA can be specified with ALL TABLES option"),
+					parser_errposition(pubobj->location));
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if "SKIP ALL TABLES IN SCHEMA" is specified
+ * with "ALL TABLES" and throw an error.
+ */
+static void
+check_skip_in_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA &&
+			pubobj->skip)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option"),
+					parser_errposition(pubobj->location));
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 893833ea83..35b28f7bcb 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1949,7 +1949,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		 * the cache entry using a historic snapshot and all the later changes
 		 * are absorbed while decoding WAL.
 		 */
-		List	   *schemaPubids = GetSchemaPublications(schemaId, objectType);
+		List	   *schemaPubids = GetSchemaPublications(schemaId, objectType, false);
+		List	   *skipSchemaPubids = GetSchemaPublications(schemaId, objectType, true);
 		ListCell   *lc;
 		Oid			publish_as_relid = relid;
 		int			publish_ancestor_level = 0;
@@ -2032,22 +2033,11 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			int	ancestor_level = 0;
 
 			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition root
-			 * and set the ancestor level accordingly. If this is a FOR ALL
-			 * SEQUENCES publication, we publish it too but we don't need to
-			 * pick the partition root etc.
+			 * If this is a FOR ALL SEQUENCES publication, we publish it too
+			 * but we don't need to pick the partition root etc.
 			 */
-			if (pub->alltables || pub->allsequences)
-			{
+			if (pub->allsequences)
 				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
 
 			if (!publish)
 			{
@@ -2067,7 +2057,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2082,6 +2073,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables && !list_member_oid(skipSchemaPubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2158,6 +2150,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(skipSchemaPubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d47fac7bb9..da39ebfcc3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5558,6 +5558,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *skipschemapuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5598,7 +5600,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	objType = pub_get_object_type_for_relkind(relkind);
 
 	puboids = list_concat_unique_oid(puboids,
-									 GetSchemaPublications(schemaid, objType));
+									 GetSchemaPublications(schemaid, objType, false));
+	skipschemapuboids = GetSchemaPublications(schemaid, objType, true);
 
 	/*
 	 * If this is a partion (and thus a table), lookup all ancestors and track
@@ -5620,10 +5623,15 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid,
-																   PUB_OBJTYPE_TABLE));
+																   PUB_OBJTYPE_TABLE, false));
+			skipschemapuboids = list_concat_unique_oid(skipschemapuboids,
+													   GetSchemaPublications(schemaid,
+																			 PUB_OBJTYPE_TABLE, true));
 		}
 	}
 
+	alltablespuboids = GetAllTablesPublications();
+
 	/*
 	 * Consider also FOR ALL TABLES and FOR ALL SEQUENCES publications,
 	 * depending on the relkind of the relation.
@@ -5631,7 +5639,9 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	if (relation->rd_rel->relkind == RELKIND_SEQUENCE)
 		puboids = list_concat_unique_oid(puboids, GetAllSequencesPublications());
 	else
-		puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+		puboids = list_concat_unique_oid(puboids,
+										 list_difference_oid(alltablespuboids,
+															 skipschemapuboids));
 
 	foreach(lc, puboids)
 	{
@@ -5662,7 +5672,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+									 pubform->pubviaroot,
+									 pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5679,7 +5690,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+									 pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 535b160165..8bed12d9b2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4040,6 +4040,7 @@ getPublicationNamespaces(Archive *fout)
 	int			i_pnpubid;
 	int			i_pnnspid;
 	int			i_pntype;
+	int			i_pnskip;
 	int			i,
 				j,
 				ntups;
@@ -4051,7 +4052,7 @@ getPublicationNamespaces(Archive *fout)
 
 	/* Collect all publication membership info. */
 	appendPQExpBufferStr(query,
-						 "SELECT tableoid, oid, pnpubid, pnnspid, pntype "
+						 "SELECT tableoid, oid, pnpubid, pnnspid, pntype, pnskip "
 						 "FROM pg_catalog.pg_publication_namespace");
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
@@ -4062,6 +4063,7 @@ getPublicationNamespaces(Archive *fout)
 	i_pnpubid = PQfnumber(res, "pnpubid");
 	i_pnnspid = PQfnumber(res, "pnnspid");
 	i_pntype = PQfnumber(res, "pntype");
+	i_pnskip = PQfnumber(res, "pnskip");
 
 	/* this allocation may be more than we need */
 	pubsinfo = pg_malloc(ntups * sizeof(PublicationSchemaInfo));
@@ -4072,6 +4074,7 @@ getPublicationNamespaces(Archive *fout)
 		Oid			pnpubid = atooid(PQgetvalue(res, i, i_pnpubid));
 		Oid			pnnspid = atooid(PQgetvalue(res, i, i_pnnspid));
 		char		pntype = PQgetvalue(res, i, i_pntype)[0];
+		char       *pnskip = pg_strdup(PQgetvalue(res, i, i_pnskip));
 		PublicationInfo *pubinfo;
 		NamespaceInfo *nspinfo;
 
@@ -4094,7 +4097,10 @@ getPublicationNamespaces(Archive *fout)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubsinfo[j].dobj.objType = DO_PUBLICATION_TABLE_IN_SCHEMA;
+		if (strcmp(pnskip, "t") == 0)
+			pubsinfo[j].dobj.objType = DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA;
+		else
+			pubsinfo[j].dobj.objType = DO_PUBLICATION_TABLE_IN_SCHEMA;
 		pubsinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4252,13 +4258,15 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
  *	  dump the definition of the given publication schema mapping.
  */
 static void
-dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
+dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo,
+						 bool bskip)
 {
 	DumpOptions *dopt = fout->dopt;
 	NamespaceInfo *schemainfo = pubsinfo->pubschema;
 	PublicationInfo *pubinfo = pubsinfo->publication;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description = (bskip) ? "PUBLICATION SKIP TABLES IN SCHEMA" : "PUBLICATION TABLES IN SCHEMA";
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4271,7 +4279,13 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	appendPQExpBuffer(query, "ALTER PUBLICATION %s ", fmtId(pubinfo->dobj.name));
 
 	if (pubsinfo->pubtype == 't')
-		appendPQExpBuffer(query, "ADD ALL TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
+	{
+		appendPQExpBufferStr(query, "ADD ");
+		if (bskip)
+			appendPQExpBufferStr(query, "SKIP ");
+
+		appendPQExpBuffer(query, "ALL TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
+	}
 	else
 		appendPQExpBuffer(query, "ADD ALL SEQUENCES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
 
@@ -4284,7 +4298,7 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = schemainfo->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLES IN SCHEMA",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9935,9 +9949,15 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
+		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
+			dumpPublicationNamespace(fout,
+									 (const PublicationSchemaInfo *) dobj,
+									 true);
+			break;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
-									 (const PublicationSchemaInfo *) dobj);
+									 (const PublicationSchemaInfo *) dobj,
+									 false);
 			break;
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
@@ -17868,6 +17888,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
+			case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
 				/* Post-data objects: must come after the post-data boundary */
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 688093c55e..24b809a8fc 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_POLICY,
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
+	DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
 } DumpableObjectType;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 1592090839..d024936b14 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -91,6 +91,7 @@ enum dbObjectTypePriorities
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
 	PRIO_PUBLICATION_REL,
+	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
 	PRIO_DEFAULT_ACL,			/* done in ACL pass */
@@ -145,6 +146,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
+	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
 };
@@ -1488,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION TABLE (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
+			snprintf(buf, bufsize,
+					 "PUBLICATION SKIP TABLES IN SCHEMA (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLES IN SCHEMA (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index af5d6fa5a3..b14864648e 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2402,6 +2402,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate, sequence');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2527,6 +2536,27 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'ALTER PUBLICATION pub6 ADD SKIP ALL TABLES IN SCHEMA dump_test' => {
+		create_order => 51,
+		create_sql =>
+		  'ALTER PUBLICATION pub6 ADD SKIP ALL TABLES IN SCHEMA dump_test;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub6 ADD SKIP ALL TABLES IN SCHEMA dump_test;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'ALTER PUBLICATION pub6 ADD SKIP ALL TABLES IN SCHEMA public' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub6 ADD SKIP ALL TABLES IN SCHEMA public;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub6 ADD SKIP ALL TABLES IN SCHEMA public;\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4dddf08789..5510c190db 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2964,7 +2964,7 @@ describeOneTableDetails(const char *schemaname,
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "		JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
-								  "WHERE pc.oid ='%s' and pn.pntype = 't' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "WHERE pc.oid ='%s' and pn.pntype = 't' AND pn.pnskip = 'f' and pg_catalog.pg_relation_is_publishable('%s')\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, pg_get_expr(pr.prqual, c.oid)\n"
@@ -6209,7 +6209,7 @@ describePublications(const char *pattern)
 								  "SELECT n.nspname\n"
 								  "FROM pg_catalog.pg_namespace n\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
-								  "WHERE pn.pnpubid = '%s' AND pn.pntype = 't'\n"
+								  "WHERE pn.pnpubid = '%s' AND pn.pntype = 't' AND pn.pnskip = 'f'\n"
 								  "ORDER BY 1", pubid);
 				if (!addFooterToPublicationDesc(&buf, "Tables from schemas:",
 												true, &cont))
@@ -6248,6 +6248,21 @@ describePublications(const char *pattern)
 			}
 		}
 
+		if (pset.sversion >= 150000)
+		{
+			/* Get the skip schemas for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT n.nspname\n"
+							  "FROM pg_catalog.pg_namespace n\n"
+							  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
+							  "WHERE pn.pnpubid = '%s'\n"
+							  "  AND pn.pnskip = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Skip tables from schemas:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 63bfdf11c6..2b7fcaca16 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1815,7 +1815,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "TABLE", "SEQUENCE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE", "SEQUENCE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
@@ -1842,11 +1844,16 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "TABLE", "SEQUENCE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE", "SEQUENCE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "TABLE", "SEQUENCE");
-	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES|SEQUENCES", "IN", "SCHEMA"))
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE", "SEQUENCE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES|SEQUENCES", "IN", "SCHEMA") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
@@ -2979,7 +2986,9 @@ psql_completion(const char *text, int start, int end)
 					  "SEQUENCE", "ALL SEQUENCES", "ALL SEQUENCES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA", "SEQUENCES", "SEQUENCES IN SCHEMA");
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES|SEQUENCES"))
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "SKIP ALL TABLES IN SCHEMA");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "SEQUENCES"))
 		COMPLETE_WITH("IN SCHEMA", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE|SEQUENCE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
@@ -3005,11 +3014,14 @@ psql_completion(const char *text, int start, int end)
 	 * Complete "CREATE PUBLICATION <name> FOR ALL TABLES|SEQUENCES IN SCHEMA <schema>,
 	 * ..."
 	 */
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES|SEQUENCES", "IN", "SCHEMA"))
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES|SEQUENCES", "IN", "SCHEMA") ||
+			 Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES|SEQUENCES", "IN", "SCHEMA", MatchAny) && (!ends_with(prev_wd, ',')))
+	else if ((Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES|SEQUENCES", "IN", "SCHEMA", MatchAny) ||
+			  Matches("CREATE", "PUBLICATION", MatchAny, "SKIP", "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny)) &&
+			 (!ends_with(prev_wd, ',')))
 		COMPLETE_WITH("WITH (");
 	/* Complete "CREATE PUBLICATION <name> [...] WITH" */
 	else if (HeadMatches("CREATE", "PUBLICATION") && TailMatches("WITH", "("))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 186d8ea74b..3c09120620 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -121,6 +121,13 @@ typedef struct PublicationRelInfo
 	List	   *columns;
 } PublicationRelInfo;
 
+typedef struct PublicationSchInfo
+{
+	NodeTag		type;
+	Oid			oid;
+	bool		skip;
+} PublicationSchInfo;
+
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
@@ -144,19 +151,21 @@ typedef enum PublicationPartOpt
 extern List *GetPublicationRelations(Oid pubid, char objectType,
 									 PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern void GetActionsInPublication(Oid pubid, PublicationActions *actions);
 extern List *GetPublicationSchemas(Oid pubid, char objectType);
-extern List *GetSchemaPublications(Oid schemaid, char objectType);
+extern List *GetSchemaPublications(Oid schemaid, char objectType, bool skippub);
 extern List *GetSchemaPublicationRelations(Oid schemaid, char objectType,
 										   PublicationPartOpt pub_partopt);
 extern List *GetAllSchemaPublicationRelations(Oid puboid, char objectType,
-											  PublicationPartOpt pub_partopt);
+											  PublicationPartOpt pub_partopt,
+											  bool bskip);
 extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level,
+											bool puballtables);
 
 extern List *GetAllSequencesPublications(void);
 extern List *GetAllSequencesPublicationRelations(void);
@@ -165,7 +174,8 @@ extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
-extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
+extern ObjectAddress publication_add_schema(Oid pubid,
+											PublicationSchInfo *pubsch,
 											char objectType,
 											bool if_not_exists);
 
diff --git a/src/include/catalog/pg_publication_namespace.h b/src/include/catalog/pg_publication_namespace.h
index 7340a1ec64..5709efa69e 100644
--- a/src/include/catalog/pg_publication_namespace.h
+++ b/src/include/catalog/pg_publication_namespace.h
@@ -33,6 +33,7 @@ CATALOG(pg_publication_namespace,8901,PublicationNamespaceRelationId)
 	Oid			pnpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			pnnspid BKI_LOOKUP(pg_namespace);	/* Oid of the schema */
 	char		pntype;								/* object type to include */
+	bool		pnskip BKI_DEFAULT(f);				/* skip objects */
 } FormData_pg_publication_namespace;
 
 /* ----------------
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index ae87caf089..0c8ad24939 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,10 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+									 List *ancestors, bool pubviaroot,
+									 bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+									 List *ancestors, bool pubviaroot,
+									 bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 5d075f0c34..18bba3bf3b 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -490,6 +490,7 @@ typedef enum NodeTag
 	T_PartitionCmd,
 	T_VacuumRelation,
 	T_PublicationObjSpec,
+	T_PublicationSchInfo,
 	T_PublicationTable,
 
 	/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5a458c42e5..4ed0121fd8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3679,6 +3679,7 @@ typedef struct PublicationObjSpec
 	PublicationObjSpecType pubobjtype;	/* type of this publication object */
 	char	   *name;
 	PublicationTable *pubtable;
+	bool		skip;
 	int			location;		/* token location, or -1 if unknown */
 } PublicationObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 0308e40ba6..8844f2619d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -84,6 +84,35 @@ DETAIL:  Tables from schema cannot be added to, dropped from, or set on FOR ALL
 ALTER PUBLICATION testpub_foralltables SET ALL TABLES IN SCHEMA pub_test;
 ERROR:  publication "testpub_foralltables" is defined as FOR ALL TABLES
 DETAIL:  Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.
+-- should be able to add skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+                                            Publication testpub_foralltables
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Sequences | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-----------+----------
+ regress_publication_user | t          | f             | t       | t       | f       | f         | f         | f
+Skip tables from schemas:
+    "pub_test"
+
+-- should be able to set skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+                                            Publication testpub_foralltables
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Sequences | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-----------+----------
+ regress_publication_user | t          | f             | t       | t       | f       | f         | f         | f
+Skip tables from schemas:
+    "public"
+
+-- should be able to drop skip schema from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+                                            Publication testpub_foralltables
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Sequences | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-----------+----------
+ regress_publication_user | t          | f             | t       | t       | f       | f         | f         | f
+(1 row)
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -119,6 +148,18 @@ ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 Tables from schemas:
     "pub_test"
 
+-- fail - can't add skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip schema from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -144,6 +185,18 @@ ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 Tables:
     "pub_test.testpub_nopk"
 
+-- fail - can't add skip schema to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip schema from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip schema to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -168,8 +221,34 @@ Publications:
  regress_publication_user | t          | f             | t       | t       | f       | f         | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skipschema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA pub_test;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_skipschema
+                                      Publication testpub_foralltables_skipschema
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Sequences | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-----------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | t         | f
+Skip tables from schemas:
+    "pub_test"
+
+-- fail - can't specify skip schema along with table publication
+CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...E pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
+-- fail - can't specify skip schema along with schema publication
+CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...BLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
+-- fail - can't specify only skip schema while create publication
+CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...N testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 96b02947fa..e2031b4ac6 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -60,6 +60,16 @@ ALTER PUBLICATION testpub_foralltables DROP ALL TABLES IN SCHEMA pub_test;
 -- fail - can't set schema to 'FOR ALL TABLES' publication
 ALTER PUBLICATION testpub_foralltables SET ALL TABLES IN SCHEMA pub_test;
 
+-- should be able to add skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+-- should be able to set skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+-- should be able to drop skip schema from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -73,6 +83,13 @@ ALTER PUBLICATION testpub_fortable DROP ALL TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 \dRp+ testpub_fortable
 
+-- fail - can't add skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't drop skip schema from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't set skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -87,12 +104,34 @@ ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk;
 ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
 
+-- fail - can't add skip schema to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't drop skip schema from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't set skip schema to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
+
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skipschema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA pub_test;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_skipschema
+
+-- fail - can't specify skip schema along with table publication
+CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+
+-- fail - can't specify skip schema along with schema publication
+CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+
+-- fail - can't specify only skip schema while create publication
+CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/032_rep_changes_skip_schema.pl b/src/test/subscription/t/032_rep_changes_skip_schema.pl
new file mode 100644
index 0000000000..7a16ad350c
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_skip_schema.pl
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for skip schema publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES SKIP ALL TABLES IN SCHEMA
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA sch1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the schema table data does not sync for skip schemas
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is skipped for skip schemas');
+
+# Insert some data into few tables and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to skip data changes in public and verify that subscriber does not get
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add SKIP ALL TABLES IN SCHEMA public");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop skip schema public and verify that subscriber gets
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop SKIP ALL TABLES IN SCHEMA public");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 85c808af90..7627b46547 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2065,6 +2065,7 @@ PublicationObjSpecType
 PublicationPartOpt
 PublicationRelInfo
 PublicationSchemaInfo
+PublicationSchInfo
 PublicationTable
 PullFilter
 PullFilterOps
-- 
2.32.0

#3vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#2)
2 attachment(s)
Re: Skipping schema changes in publication

On Sat, Mar 26, 2022 at 7:37 PM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Mar 22, 2022 at 12:38 PM vignesh C <vignesh21@gmail.com> wrote:

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.

The patch was not applying on top of HEAD because of the recent
commits, attached patch is rebased on top of HEAD.

The patch does not apply on top of HEAD because of the recent commit,
attached patch is rebased on top of HEAD.

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

Regards,
Vignesh

Attachments:

v1-0001-Skip-publishing-the-tables-of-schema.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Skip-publishing-the-tables-of-schema.patchDownload
From fecbde6558e11938537fcf896096eb3e455997c9 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 12 Apr 2022 10:40:29 +0530
Subject: [PATCH v1 1/2] Skip publishing the tables of schema.

A new option "SKIP ALL TABLES IN SCHEMA" in Create/Alter Publication allows
one or more skip schemas to be specified, publisher will skip sending the data
of the tables present in the skip schema to the subscriber.

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to maintain
the schemas that the user wants to skip publishing through the publication.
Modified the output plugin (pgoutput) to skip publishing the changes if the
relation is part of skip schema publication.

Updates pg_dump to identify and dump skip schema publications. Updates the \d
family of commands to display skip schema publications and \dRp+ variant will
now display associated skip schemas if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 ++
 doc/src/sgml/logical-replication.sgml         |   7 +-
 doc/src/sgml/ref/alter_publication.sgml       |  28 +++-
 doc/src/sgml/ref/create_publication.sgml      |  28 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  65 ++++++---
 src/backend/commands/publicationcmds.c        | 128 +++++++++++-------
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/nodes/copyfuncs.c                 |  14 ++
 src/backend/nodes/equalfuncs.c                |  14 ++
 src/backend/parser/gram.y                     | 101 +++++++++++++-
 src/backend/replication/pgoutput/pgoutput.c   |  24 +---
 src/backend/utils/cache/relcache.c            |  20 ++-
 src/bin/pg_dump/pg_dump.c                     |  30 +++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 ++++
 src/bin/psql/describe.c                       |  26 +++-
 src/bin/psql/tab-complete.c                   |  24 +++-
 src/include/catalog/pg_publication.h          |  20 ++-
 .../catalog/pg_publication_namespace.h        |   1 +
 src/include/commands/publicationcmds.h        |   6 +-
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/test/regress/expected/publication.out     |  81 ++++++++++-
 src/test/regress/sql/publication.sql          |  41 +++++-
 .../t/032_rep_changes_skip_schema.pl          |  96 +++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 28 files changed, 686 insertions(+), 125 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_skip_schema.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6f285871b6..1b02af1c03 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6438,6 +6438,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        A null value indicates that all columns are published.
       </para></entry>
      </row>
+
+    <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pnskip</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the schema is skip schema
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 555fbd749c..e2a4b89226 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -599,9 +599,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
 
   <para>
    To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   table. To add all tables in schema or skip all tables in schema to a
+   publication, the user must be a superuser. To create a publication that
+   publishes all tables or all tables in schema automatically, the user must be
+   a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..6f915d8c5d 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -31,7 +31,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
-    ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+    [SKIP] ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -71,12 +71,12 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
+   The <literal>ADD [SKIP] ALL TABLES IN SCHEMA</literal> and
+   <literal>SET [SKIP] ALL TABLES IN SCHEMA</literal> to a publication requires
+   the invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
    <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
+   of a <literal>FOR ALL TABLES</literal> or <literal>FOR [SKIP] ALL TABLES IN
    SCHEMA</literal> publication must be a superuser. However, a superuser can
    change the ownership of a publication regardless of these restrictions.
   </para>
@@ -88,6 +88,14 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    adding/setting a table to a publication that already has a table's schema as
    part of the specified schema is not supported.
   </para>
+
+  <para>
+   The <literal>ADD SKIP ALL TABLES IN SCHEMA</literal> and
+   <literal>SET SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+   <literal>FOR ALL TABLES</literal> publication. It is not supported for
+   <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+   <literal>FOR TABLE</literal> publication.
+  </para>
  </refsect1>
 
  <refsect1>
@@ -217,6 +225,16 @@ ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLE
    <command>ALTER PUBLICATION</command> is a <productname>PostgreSQL</productname>
    extension.
   </para>
+
+   <para>
+   Add skip schemas <structname>sales_june</structname> and
+   <structname>sales_july</structname> to the publication
+   <structname>mypublication</structname>:
+<programlisting>
+ALTER PUBLICATION mypublication ADD SKIP ALL TABLES IN SCHEMA sales_june, sales_july;
+</programlisting>
+  </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index fb2d013393..5acf80452a 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA }]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -124,6 +124,23 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>SKIP ALL TABLES IN SCHEMA</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that skips replicating changes for all
+      tables in the specified list of schemas.
+     </para>
+
+     <para>
+      <literal>SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>FOR ALL TABLES IN SCHEMA</literal></term>
     <listitem>
@@ -334,6 +351,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
    <structname>sales</structname>:
 <programlisting>
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of all the tables present in the schema
+   <structname>marketing</structname> and <structname>sales</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE SKIP ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 592356019b..b097b863cd 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        schemas and the skip schema associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 2631558ff1..3d2cab47a6 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -304,6 +305,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		Oid			ancestor = lfirst_oid(lc);
 		List	   *apubids = GetRelationPublications(ancestor);
 		List	   *aschemaPubids = NIL;
+		List       *askipschemaPubids = NIL;
 
 		level++;
 
@@ -316,8 +318,11 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		}
 		else
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor), false);
+			askipschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor), true);
+
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(askipschemaPubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -585,13 +590,14 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Insert new publication / schema mapping.
  */
 ObjectAddress
-publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
+publication_add_schema(Oid pubid, PublicationSchInfo *pubsch, bool if_not_exists)
 {
 	Relation	rel;
 	HeapTuple	tup;
 	Datum		values[Natts_pg_publication_namespace];
 	bool		nulls[Natts_pg_publication_namespace];
 	Oid			psschid;
+	Oid 		schemaid = pubsch->oid;
 	Publication *pub = GetPublication(pubid);
 	List	   *schemaRels = NIL;
 	ObjectAddress myself,
@@ -632,6 +638,8 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_namespace_pnnspid - 1] =
 		ObjectIdGetDatum(schemaid);
+	values[Anum_pg_publication_namespace_pnskip - 1] =
+		BoolGetDatum(pubsch->skip);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -779,13 +787,23 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *skipschemaidlist = NIL;
+	List	   *pubschemalist = GetPublicationSchemas(pubid);
+	ListCell   *cell;
+
+	foreach(cell, pubschemalist)
+	{
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
+
+		skipschemaidlist = lappend_oid(result, pubsch->oid);
+	}
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -800,9 +818,11 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	{
 		Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 		Oid			relid = relForm->oid;
+		Oid			schid = get_rel_namespace(relid);
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(skipschemaidlist, schid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -821,9 +841,11 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		{
 			Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 			Oid			relid = relForm->oid;
+			Oid			schid = get_rel_namespace(relid);
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(skipschemaidlist, schid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -862,10 +884,14 @@ GetPublicationSchemas(Oid pubid)
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_namespace pubsch;
+		PublicationSchInfo *schinfo = makeNode(PublicationSchInfo);
+
 
 		pubsch = (Form_pg_publication_namespace) GETSTRUCT(tup);
+		schinfo->oid = pubsch->pnnspid;
+		schinfo->skip = pubsch->pnskip;
 
-		result = lappend_oid(result, pubsch->pnnspid);
+		result = lappend(result, schinfo);
 	}
 
 	systable_endscan(scan);
@@ -878,7 +904,7 @@ GetPublicationSchemas(Oid pubid)
  * Gets the list of publication oids associated with a specified schema.
  */
 List *
-GetSchemaPublications(Oid schemaid)
+GetSchemaPublications(Oid schemaid, bool skippub)
 {
 	List	   *result = NIL;
 	CatCList   *pubschlist;
@@ -892,7 +918,8 @@ GetSchemaPublications(Oid schemaid)
 		HeapTuple	tup = &pubschlist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_namespace) GETSTRUCT(tup))->pnpubid;
 
-		result = lappend_oid(result, pubid);
+		if (skippub == ((Form_pg_publication_namespace) GETSTRUCT(tup))->pnskip)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubschlist);
@@ -961,7 +988,7 @@ GetSchemaPublicationRelations(Oid schemaid, PublicationPartOpt pub_partopt)
  * publication.
  */
 List *
-GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt, bool bskip)
 {
 	List	   *result = NIL;
 	List	   *pubschemalist = GetPublicationSchemas(pubid);
@@ -969,11 +996,15 @@ GetAllSchemaPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 
 	foreach(cell, pubschemalist)
 	{
-		Oid			schemaid = lfirst_oid(cell);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
 		List	   *schemaRels = NIL;
 
-		schemaRels = GetSchemaPublicationRelations(schemaid, pub_partopt);
-		result = list_concat(result, schemaRels);
+		/* Skip the skip schemas if bskip is true */
+		if (bskip && !pubsch->skip)
+		{
+			schemaRels = GetSchemaPublicationRelations(pubsch->oid, pub_partopt);
+			result = list_concat(result, schemaRels);
+		}
 	}
 
 	return result;
@@ -1107,7 +1138,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
@@ -1121,7 +1153,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			schemarelids = GetAllSchemaPublicationRelations(publication->oid,
 															publication->pubviaroot ?
 															PUBLICATION_PART_ROOT :
-															PUBLICATION_PART_LEAF);
+															PUBLICATION_PART_LEAF,
+															true);
 			tables = list_concat_unique_oid(relids, schemarelids);
 
 			/*
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 7aacb6b2fe..6863900bd9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -177,8 +177,8 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 
 	foreach(cell, pubobjspec_list)
 	{
-		Oid			schemaid;
 		List	   *search_path;
+		PublicationSchInfo *pubsch = makeNode(PublicationSchInfo);
 
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
@@ -188,10 +188,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
-				schemaid = get_namespace_oid(pubobj->name, false);
+				pubsch->oid = get_namespace_oid(pubobj->name, false);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*schemas = list_append_unique_oid(*schemas, schemaid);
+				*schemas = list_append_unique(*schemas, pubsch);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA:
 				search_path = fetch_search_path(false);
@@ -200,11 +201,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 							errcode(ERRCODE_UNDEFINED_SCHEMA),
 							errmsg("no schema has been selected for CURRENT_SCHEMA"));
 
-				schemaid = linitial_oid(search_path);
+				pubsch->oid = linitial_oid(search_path);
 				list_free(search_path);
+				pubsch->skip = pubobj->skip;
 
 				/* Filter out duplicates if user specifies "sch1, sch1" */
-				*schemas = list_append_unique_oid(*schemas, schemaid);
+				*schemas = list_append_unique(*schemas, pubsch);
 				break;
 			default:
 				/* shouldn't happen */
@@ -230,24 +232,29 @@ CheckObjSchemaNotAlreadyInPublication(List *rels, List *schemaidlist,
 		Relation	rel = pub_rel->relation;
 		Oid			relSchemaId = RelationGetNamespace(rel);
 
-		if (list_member_oid(schemaidlist, relSchemaId))
+		foreach(lc, schemaidlist)
 		{
-			if (checkobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add schema \"%s\" to publication",
-							   get_namespace_name(relSchemaId)),
-						errdetail("Table \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
-								  RelationGetRelationName(rel),
-								  get_namespace_name(relSchemaId)));
-			else if (checkobjtype == PUBLICATIONOBJ_TABLE)
-				ereport(ERROR,
-						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						errmsg("cannot add relation \"%s.%s\" to publication",
-							   get_namespace_name(relSchemaId),
-							   RelationGetRelationName(rel)),
-						errdetail("Table's schema \"%s\" is already part of the publication or part of the specified schema list.",
-								  get_namespace_name(relSchemaId)));
+			PublicationSchInfo *pub_sch = (PublicationSchInfo *) lfirst(lc);
+
+			if (pub_sch->oid == relSchemaId)
+			{
+				if (checkobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add schema \"%s\" to publication",
+								get_namespace_name(relSchemaId)),
+							errdetail("Table \"%s\" in schema \"%s\" is already part of the publication, adding the same schema is not supported.",
+									RelationGetRelationName(rel),
+									get_namespace_name(relSchemaId)));
+				else if (checkobjtype == PUBLICATIONOBJ_TABLE)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("cannot add relation \"%s.%s\" to publication",
+								get_namespace_name(relSchemaId),
+								RelationGetRelationName(rel)),
+							errdetail("Table's schema \"%s\" is already part of the publication or part of the specified schema list.",
+									get_namespace_name(relSchemaId)));
+			}
 		}
 	}
 }
@@ -297,7 +304,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+						 bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -324,7 +331,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -373,7 +381,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+						 bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -392,7 +400,7 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -835,18 +843,21 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
+
 	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
 	{
+		Assert(!relations);
+
 		/* Invalidate relcache so that publication info is rebuilt. */
 		CacheInvalidateRelcacheAll();
 	}
 	else
 	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
+		/* FOR [SKIP] ALL TABLES IN SCHEMA requires superuser */
 		if (list_length(schemaidlist) > 0 && !superuser())
 			ereport(ERROR,
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -869,16 +880,17 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 			PublicationAddTables(puboid, rels, true, NULL);
 			CloseTableList(rels);
 		}
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	/* tables added through a schema */
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
@@ -1069,7 +1081,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		}
 
 		schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
-														PUBLICATION_PART_ALL);
+														PUBLICATION_PART_ALL,
+														false);
 		relids = list_concat_unique_oid(relids, schemarelids);
 
 		InvalidatePublicationRels(relids);
@@ -1133,7 +1146,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		 * Check if the relation is member of the existing schema in the
 		 * publication or member of the schema list specified.
 		 */
-		schemas = list_concat_copy(schemaidlist, GetPublicationSchemas(pubid));
+		schemas = list_concat(schemaidlist, GetPublicationSchemas(pubid));
 		CheckObjSchemaNotAlreadyInPublication(rels, schemas,
 											  PUBLICATIONOBJ_TABLE);
 
@@ -1326,7 +1339,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		List	   *delschemas = NIL;
 
 		/* Identify which schemas should be dropped */
-		delschemas = list_difference_oid(oldschemaids, schemaidlist);
+		delschemas = list_difference(oldschemaids, schemaidlist);
 
 		/*
 		 * Schema lock is held until the publication is altered to prevent
@@ -1354,6 +1367,20 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *tables, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+
+	bool		nonskipschema = false;
+	bool		skipschema = false;
+
+	foreach(lc, schemaidlist)
+	{
+		PublicationSchInfo *pub_sch = (PublicationSchInfo *) lfirst(lc);
+
+		if (!pub_sch->skip)
+			nonskipschema = true;
+		else
+			skipschema = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
@@ -1365,13 +1392,20 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 	 * Check that user is allowed to manipulate the publication tables in
 	 * schema
 	 */
-	if (schemaidlist && pubform->puballtables)
+	if (nonskipschema && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
+	if (skipschema && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				errdetail("Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
+
 	/* Check that user is allowed to manipulate the publication tables. */
 	if (tables && pubform->puballtables)
 		ereport(ERROR,
@@ -1775,7 +1809,8 @@ LockSchemaList(List *schemalist)
 
 	foreach(lc, schemalist)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
+		Oid			schemaid = pubsch->oid;
 
 		/* Allow query cancel in case this takes a long time */
 		CHECK_FOR_INTERRUPTS();
@@ -1885,10 +1920,10 @@ PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists,
 
 	foreach(lc, schemas)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
 		ObjectAddress obj;
 
-		obj = publication_add_schema(pubid, schemaid, if_not_exists);
+		obj = publication_add_schema(pubid, pubsch, if_not_exists);
 		if (stmt)
 		{
 			EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress,
@@ -1912,7 +1947,8 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
 
 	foreach(lc, schemas)
 	{
-		Oid			schemaid = lfirst_oid(lc);
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(lc);
+		Oid			schemaid = pubsch->oid;
 
 		psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP,
 							   Anum_pg_publication_namespace_oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 90edd0bb97..141a2eabf8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16412,7 +16412,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	if (stmt->objectType == OBJECT_TABLE)
 	{
 		ListCell   *lc;
-		List	   *schemaPubids = GetSchemaPublications(nspOid);
+		List	   *schemaPubids = GetSchemaPublications(nspOid, false);
 		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
 
 		foreach(lc, relPubids)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 836f427ea8..e2369bb059 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -5386,6 +5386,17 @@ _copyPublicationTable(const PublicationTable *from)
 	return newnode;
 }
 
+static PublicationSchInfo *
+_copyPublicationSchInfo(const PublicationSchInfo *from)
+{
+	PublicationSchInfo *newnode = makeNode(PublicationSchInfo);
+
+	COPY_SCALAR_FIELD(oid);
+	COPY_SCALAR_FIELD(skip);
+
+	return newnode;
+}
+
 static CreatePublicationStmt *
 _copyCreatePublicationStmt(const CreatePublicationStmt *from)
 {
@@ -6565,6 +6576,9 @@ copyObjectImpl(const void *from)
 		case T_PublicationObjSpec:
 			retval = _copyPublicationObject(from);
 			break;
+		case T_PublicationSchInfo:
+			retval = _copyPublicationSchInfo(from);
+			break;
 		case T_PublicationTable:
 			retval = _copyPublicationTable(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index e013c1bbfe..e19ce69a26 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -33,6 +33,7 @@
 #include "nodes/extensible.h"
 #include "nodes/pathnodes.h"
 #include "utils/datum.h"
+#include "catalog/pg_publication.h"
 
 
 /*
@@ -2681,6 +2682,16 @@ _equalPublicationTable(const PublicationTable *a, const PublicationTable *b)
 	return true;
 }
 
+static bool
+_equalPublicationSchema(const PublicationSchInfo *a,
+						const PublicationSchInfo *b)
+{
+	COMPARE_SCALAR_FIELD(oid);
+	COMPARE_SCALAR_FIELD(skip);
+
+	return true;
+}
+
 static bool
 _equalCreatePublicationStmt(const CreatePublicationStmt *a,
 							const CreatePublicationStmt *b)
@@ -4368,6 +4379,9 @@ equal(const void *a, const void *b)
 		case T_PublicationObjSpec:
 			retval = _equalPublicationObject(a, b);
 			break;
+		case T_PublicationSchInfo:
+			retval = _equalPublicationSchema(a, b);
+			break;
 		case T_PublicationTable:
 			retval = _equalPublicationTable(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4..7fd8d194ce 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -219,6 +219,11 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
+static void preprocess_alltables_pubobj_list(List *pubobjspec_list,
+											 int location,
+											 core_yyscan_t yyscanner);
+static void check_skip_in_pubobj_list(List *pubobjspec_list,
+											 core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %}
@@ -455,7 +460,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list skip_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -9879,12 +9884,17 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *)n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES skip_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
+					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					preprocess_alltables_pubobj_list(n->pubobjects,
+													 @6,
+													 yyscanner);
 					$$ = (Node *)n;
 				}
 			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
@@ -9894,6 +9904,7 @@ CreatePublicationStmt:
 					n->options = $6;
 					n->pubobjects = (List *)$5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_skip_in_pubobj_list(n->pubobjects, yyscanner);
 					$$ = (Node *)n;
 				}
 		;
@@ -9925,6 +9936,7 @@ PublicationObjSpec:
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
+					$$->skip = false;
 					$$->name = $5;
 					$$->location = @5;
 				}
@@ -9932,8 +9944,25 @@ PublicationObjSpec:
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->skip = false;
 					$$->location = @5;
 				}
+			| SKIP ALL TABLES IN_P SCHEMA ColId
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
+					$$->name = $6;
+					$$->skip = true;
+					$$->location = @6;
+				}
+			| SKIP ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->skip = true;
+					$$->location = @6;
+				}
+
 			| ColId opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10257,6 +10286,12 @@ opt_instead:
 		;
 
 
+ skip_pub_obj_list:	pub_obj_list
+						{ $$ = $1; }
+					| /*EMPTY*/
+						{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -18712,6 +18747,7 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	ListCell   *cell;
 	PublicationObjSpec *pubobj;
 	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_CONTINUATION;
+	bool prevskipobj = false;
 
 	if (!pubobjspec_list)
 		return;
@@ -18729,7 +18765,10 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_CONTINUATION)
+		{
 			pubobj->pubobjtype = prevobjtype;
+			pubobj->skip = prevskipobj;
+		}
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
 		{
@@ -18784,6 +18823,62 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		}
 
 		prevobjtype = pubobj->pubobjtype;
+		prevskipobj = pubobj->skip;
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if any other option other that
+ * "SKIP ALL TABLES IN SCHEMA" is specified with "ALL TABLES" and throw an
+ * error.
+ */
+static void
+preprocess_alltables_pubobj_list(List *pubobjspec_list, int location,
+								 core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		PublicationObjSpec *pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
+			!pubobj->skip)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("only SKIP ALL TABLES IN SCHEMA can be specified with ALL TABLES option"),
+					parser_errposition(pubobj->location));
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if "SKIP ALL TABLES IN SCHEMA" is specified
+ * with "ALL TABLES" and throw an error.
+ */
+static void
+check_skip_in_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA &&
+			pubobj->skip)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option"),
+					parser_errposition(pubobj->location));
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index fe5accca57..19181297af 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1996,7 +1996,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		 * the cache entry using a historic snapshot and all the later changes
 		 * are absorbed while decoding WAL.
 		 */
-		List	   *schemaPubids = GetSchemaPublications(schemaId);
+		List	   *schemaPubids = GetSchemaPublications(schemaId, false);
+		List       *skipSchemaPubids = GetSchemaPublications(schemaId, true);
 		ListCell   *lc;
 		Oid			publish_as_relid = relid;
 		int			publish_ancestor_level = 0;
@@ -2078,22 +2079,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid	pub_relid = relid;
 			int	ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition root
-			 * and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2112,7 +2097,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2127,6 +2113,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables && !list_member_oid(skipSchemaPubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2202,6 +2189,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(skipSchemaPubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43f14c233d..02c0a47ca1 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5562,6 +5562,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *skipschemapuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5597,7 +5599,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	/* Fetch the publication membership info. */
 	puboids = GetRelationPublications(relid);
 	schemaid = RelationGetNamespace(relation);
-	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid, false));
+	skipschemapuboids = GetSchemaPublications(schemaid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5612,11 +5615,17 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 											 GetRelationPublications(ancestor));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
-											 GetSchemaPublications(schemaid));
+											 GetSchemaPublications(schemaid, false));
+			skipschemapuboids = list_concat_unique_oid(skipschemapuboids,
+													   GetSchemaPublications(schemaid,
+																			 true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 skipschemapuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5645,7 +5654,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+									 pubform->pubviaroot,
+									 pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5662,7 +5672,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+									 pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 969e2a7a46..296527bde5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4004,6 +4004,7 @@ getPublicationNamespaces(Archive *fout)
 	int			i_oid;
 	int			i_pnpubid;
 	int			i_pnnspid;
+	int			i_pnskip;
 	int			i,
 				j,
 				ntups;
@@ -4015,7 +4016,7 @@ getPublicationNamespaces(Archive *fout)
 
 	/* Collect all publication membership info. */
 	appendPQExpBufferStr(query,
-						 "SELECT tableoid, oid, pnpubid, pnnspid "
+						 "SELECT tableoid, oid, pnpubid, pnnspid, pnskip "
 						 "FROM pg_catalog.pg_publication_namespace");
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
@@ -4025,6 +4026,7 @@ getPublicationNamespaces(Archive *fout)
 	i_oid = PQfnumber(res, "oid");
 	i_pnpubid = PQfnumber(res, "pnpubid");
 	i_pnnspid = PQfnumber(res, "pnnspid");
+	i_pnskip = PQfnumber(res, "pnskip");
 
 	/* this allocation may be more than we need */
 	pubsinfo = pg_malloc(ntups * sizeof(PublicationSchemaInfo));
@@ -4034,6 +4036,7 @@ getPublicationNamespaces(Archive *fout)
 	{
 		Oid			pnpubid = atooid(PQgetvalue(res, i, i_pnpubid));
 		Oid			pnnspid = atooid(PQgetvalue(res, i, i_pnnspid));
+		char       *pnskip = pg_strdup(PQgetvalue(res, i, i_pnskip));
 		PublicationInfo *pubinfo;
 		NamespaceInfo *nspinfo;
 
@@ -4056,7 +4059,10 @@ getPublicationNamespaces(Archive *fout)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubsinfo[j].dobj.objType = DO_PUBLICATION_TABLE_IN_SCHEMA;
+		if (strcmp(pnskip, "t") == 0)
+			pubsinfo[j].dobj.objType = DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA;
+		else
+			pubsinfo[j].dobj.objType = DO_PUBLICATION_TABLE_IN_SCHEMA;
 		pubsinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4213,13 +4219,15 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
  *	  dump the definition of the given publication schema mapping.
  */
 static void
-dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
+dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo,
+						 bool bskip)
 {
 	DumpOptions *dopt = fout->dopt;
 	NamespaceInfo *schemainfo = pubsinfo->pubschema;
 	PublicationInfo *pubinfo = pubsinfo->publication;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description = (bskip) ? "PUBLICATION SKIP TABLES IN SCHEMA" : "PUBLICATION TABLES IN SCHEMA";
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4230,8 +4238,11 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	query = createPQExpBuffer();
 
 	appendPQExpBuffer(query, "ALTER PUBLICATION %s ", fmtId(pubinfo->dobj.name));
-	appendPQExpBuffer(query, "ADD ALL TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
+	appendPQExpBufferStr(query, "ADD ");
+	if (bskip)
+		appendPQExpBufferStr(query, "SKIP ");
 
+	appendPQExpBuffer(query, "ALL TABLES IN SCHEMA %s;\n", fmtId(schemainfo->dobj.name));
 	/*
 	 * There is no point in creating drop query as the drop is done by schema
 	 * drop.
@@ -4241,7 +4252,7 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = schemainfo->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLES IN SCHEMA",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9880,9 +9891,15 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
+		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
+			dumpPublicationNamespace(fout,
+									 (const PublicationSchemaInfo *) dobj,
+									 true);
+			break;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
-									 (const PublicationSchemaInfo *) dobj);
+									 (const PublicationSchemaInfo *) dobj,
+									 false);
 			break;
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
@@ -17811,6 +17828,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
+			case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
 				/* Post-data objects: must come after the post-data boundary */
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..cb9e5e164b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_POLICY,
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
+	DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
 } DumpableObjectType;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..47d4baecb3 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -91,6 +91,7 @@ enum dbObjectTypePriorities
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
 	PRIO_PUBLICATION_REL,
+	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
 	PRIO_DEFAULT_ACL,			/* done in ACL pass */
@@ -145,6 +146,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
+	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
 };
@@ -1488,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION TABLE (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
+			snprintf(buf, bufsize,
+					 "PUBLICATION SKIP TABLES IN SCHEMA (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLES IN SCHEMA (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c65c92bfb0..2dbb43c30e 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2558,6 +2567,27 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA dump_test' => {
+		create_order => 51,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA dump_test;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA dump_test;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA public' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA public;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD SKIP ALL TABLES IN SCHEMA public;\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e7377d4583..dd6b63e7e2 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2898,7 +2898,7 @@ describeOneTableDetails(const char *schemaname,
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "		JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
-								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "WHERE pc.oid ='%s' AND pn.pnskip = 'f' and pg_catalog.pg_relation_is_publishable('%s')\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, pg_get_expr(pr.prqual, c.oid)\n"
@@ -2918,8 +2918,13 @@ describeOneTableDetails(const char *schemaname,
 								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "		AND NOT EXISTS (SELECT 1\n"
+								  "							FROM pg_catalog.pg_publication_namespace pn\n"
+								  "								JOIN pg_catalog.pg_class pc\n"
+								  "	  	 						ON pc.relnamespace = pn.pnnspid\n"
+								  "							WHERE pc.oid ='%s' AND pn.pnpubid = p.oid)\n"
 								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  oid, oid, oid, oid, oid);
 			}
 			else
 			{
@@ -6165,7 +6170,7 @@ describePublications(const char *pattern)
 								  "SELECT n.nspname\n"
 								  "FROM pg_catalog.pg_namespace n\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
-								  "WHERE pn.pnpubid = '%s'\n"
+								  "WHERE pn.pnpubid = '%s' AND pn.pnskip = 'f'\n"
 								  "ORDER BY 1", pubid);
 				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
 												true, &cont))
@@ -6173,6 +6178,21 @@ describePublications(const char *pattern)
 			}
 		}
 
+		if (pset.sversion >= 150000)
+		{
+			/* Get the skip schemas for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT n.nspname\n"
+							  "FROM pg_catalog.pg_namespace n\n"
+							  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
+							  "WHERE pn.pnpubid = '%s'\n"
+							  "  AND pn.pnskip = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Skip tables from schemas:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 10e8dbc9b1..402ab90b58 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1828,7 +1828,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
@@ -1851,11 +1853,16 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
-	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
@@ -2991,7 +2998,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "SKIP ALL TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
@@ -3013,11 +3020,14 @@ psql_completion(const char *text, int start, int end)
 	 * Complete "CREATE PUBLICATION <name> FOR ALL TABLES IN SCHEMA <schema>,
 	 * ..."
 	 */
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA") ||
+			 Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
-	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) && (!ends_with(prev_wd, ',')))
+	else if ((Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) ||
+			  Matches("CREATE", "PUBLICATION", MatchAny, "SKIP", "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny)) &&
+			 (!ends_with(prev_wd, ',')))
 		COMPLETE_WITH("WITH (");
 	/* Complete "CREATE PUBLICATION <name> [...] WITH" */
 	else if (HeadMatches("CREATE", "PUBLICATION") && TailMatches("WITH", "("))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 29b1856665..30a2fcb974 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -110,6 +110,13 @@ typedef struct PublicationRelInfo
 	List	   *columns;
 } PublicationRelInfo;
 
+typedef struct PublicationSchInfo
+{
+	NodeTag		type;
+	Oid			oid;
+	bool		skip;
+} PublicationSchInfo;
+
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
@@ -132,24 +139,27 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
-extern List *GetSchemaPublications(Oid schemaid);
+extern List *GetSchemaPublications(Oid schemaid, bool skippub);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
 										   PublicationPartOpt pub_partopt);
 extern List *GetAllSchemaPublicationRelations(Oid puboid,
-											  PublicationPartOpt pub_partopt);
+											  PublicationPartOpt pub_partopt,
+											  bool bskip);
 extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level,
+											bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
-extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
+extern ObjectAddress publication_add_schema(Oid pubid,
+											PublicationSchInfo *pubsch,
 											bool if_not_exists);
 
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
diff --git a/src/include/catalog/pg_publication_namespace.h b/src/include/catalog/pg_publication_namespace.h
index e4306da02e..99c1b7e095 100644
--- a/src/include/catalog/pg_publication_namespace.h
+++ b/src/include/catalog/pg_publication_namespace.h
@@ -32,6 +32,7 @@ CATALOG(pg_publication_namespace,8901,PublicationNamespaceRelationId)
 	Oid			oid;			/* oid */
 	Oid			pnpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			pnnspid BKI_LOOKUP(pg_namespace);	/* Oid of the schema */
+	bool		pnskip BKI_DEFAULT(f);				/* skip objects */
 } FormData_pg_publication_namespace;
 
 /* ----------------
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index ae87caf089..0c8ad24939 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,10 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+									 List *ancestors, bool pubviaroot,
+									 bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+									 List *ancestors, bool pubviaroot,
+									 bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 340d28f4e1..9538389d53 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -507,6 +507,7 @@ typedef enum NodeTag
 	T_PartitionCmd,
 	T_VacuumRelation,
 	T_PublicationObjSpec,
+	T_PublicationSchInfo,
 	T_PublicationTable,
 	T_JsonObjectConstructor,
 	T_JsonArrayConstructor,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index da02658c81..188285c99d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4023,6 +4023,7 @@ typedef struct PublicationObjSpec
 	PublicationObjSpecType pubobjtype;	/* type of this publication object */
 	char	   *name;
 	PublicationTable *pubtable;
+	bool		skip;
 	int			location;		/* token location, or -1 if unknown */
 } PublicationObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 8208f9fa0e..e7e46d0841 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -81,6 +81,35 @@ DETAIL:  Tables from schema cannot be added to, dropped from, or set on FOR ALL
 ALTER PUBLICATION testpub_foralltables SET ALL TABLES IN SCHEMA pub_test;
 ERROR:  publication "testpub_foralltables" is defined as FOR ALL TABLES
 DETAIL:  Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.
+-- should be able to add skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Skip tables from schemas:
+    "pub_test"
+
+-- should be able to set skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Skip tables from schemas:
+    "public"
+
+-- should be able to drop skip schema from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+(1 row)
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -116,6 +145,18 @@ ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 Tables from schemas:
     "pub_test"
 
+-- fail - can't add skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip schema from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -141,6 +182,18 @@ ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 Tables:
     "pub_test.testpub_nopk"
 
+-- fail - can't add skip schema to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip schema from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip schema to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -165,8 +218,34 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skipschema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA pub_test;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_skipschema
+                        Publication testpub_foralltables_skipschema
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Skip tables from schemas:
+    "pub_test"
+
+-- fail - can't specify skip schema along with table publication
+CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...E pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
+-- fail - can't specify skip schema along with schema publication
+CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...BLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
+-- fail - can't specify only skip schema while create publication
+CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+LINE 1: ...N testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+                                                              ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 8539110025..8d8a522c76 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -58,6 +58,16 @@ ALTER PUBLICATION testpub_foralltables DROP ALL TABLES IN SCHEMA pub_test;
 -- fail - can't set schema to 'FOR ALL TABLES' publication
 ALTER PUBLICATION testpub_foralltables SET ALL TABLES IN SCHEMA pub_test;
 
+-- should be able to add skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP ALL TABLES IN SCHEMA pub_test;
+\dRp+ testpub_foralltables
+-- should be able to set skip schema to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+-- should be able to drop skip schema from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA public;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -71,6 +81,13 @@ ALTER PUBLICATION testpub_fortable DROP ALL TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 \dRp+ testpub_fortable
 
+-- fail - can't add skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't drop skip schema from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't set skip schema to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -85,12 +102,34 @@ ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk;
 ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
 
+-- fail - can't add skip schema to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't drop skip schema from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
+-- fail - can't set skip schema to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
+
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skipschema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA pub_test;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_skipschema
+
+-- fail - can't specify skip schema along with table publication
+CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
+
+-- fail - can't specify skip schema along with schema publication
+CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
+
+-- fail - can't specify only skip schema while create publication
+CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/032_rep_changes_skip_schema.pl b/src/test/subscription/t/032_rep_changes_skip_schema.pl
new file mode 100644
index 0000000000..7a16ad350c
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_skip_schema.pl
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for skip schema publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES SKIP ALL TABLES IN SCHEMA
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES SKIP ALL TABLES IN SCHEMA sch1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the schema table data does not sync for skip schemas
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is skipped for skip schemas');
+
+# Insert some data into few tables and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to skip data changes in public and verify that subscriber does not get
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add SKIP ALL TABLES IN SCHEMA public");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop skip schema public and verify that subscriber gets
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop SKIP ALL TABLES IN SCHEMA public");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 87ee7bf866..b23f21bf96 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2073,6 +2073,7 @@ PublicationObjSpecType
 PublicationPartOpt
 PublicationRelInfo
 PublicationSchemaInfo
+PublicationSchInfo
 PublicationTable
 PullFilter
 PullFilterOps
-- 
2.32.0

v1-0002-Skip-publishing-the-tables.patchtext/x-patch; charset=US-ASCII; name=v1-0002-Skip-publishing-the-tables.patchDownload
From 604db8ac5ce06da98ab6aeb19915b239a7541a83 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 12 Apr 2022 10:55:34 +0530
Subject: [PATCH v1 2/2] Skip publishing the tables.

A new option "SKIP TABLE" in Create/Alter Publication allows
one or more skip tables to be specified, publisher will skip sending the data
of the tables present in the skip table to the subscriber.

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

A new column pnskip is added to table "pg_publication_rel", to maintain
the relations that the user wants to skip publishing through the publication.
Modified the output plugin (pgoutput) to skip publishing the changes if the
relation is part of skip table publication.

Updates pg_dump to identify and dump skip table publications. Updates the \d
family of commands to display skip table publications and \dRp+ variant will
now display associated skip tables if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    | 13 ++-
 doc/src/sgml/logical-replication.sgml         | 10 +-
 doc/src/sgml/ref/alter_publication.sgml       | 22 +++--
 doc/src/sgml/ref/create_publication.sgml      | 29 +++++-
 doc/src/sgml/ref/psql-ref.sgml                |  4 +-
 src/backend/catalog/pg_publication.c          | 31 ++++--
 src/backend/commands/publicationcmds.c        | 62 +++++++-----
 src/backend/commands/tablecmds.c              |  4 +-
 src/backend/parser/gram.y                     | 45 +++++----
 src/backend/replication/pgoutput/pgoutput.c   |  8 +-
 src/backend/utils/cache/relcache.c            | 18 ++--
 src/bin/pg_dump/pg_dump.c                     | 35 +++++--
 src/bin/pg_dump/pg_dump.h                     |  1 +
 src/bin/pg_dump/pg_dump_sort.c                |  7 ++
 src/bin/pg_dump/t/002_pg_dump.pl              | 23 +++++
 src/bin/psql/describe.c                       | 22 ++++-
 src/bin/psql/tab-complete.c                   | 17 ++--
 src/include/catalog/pg_publication.h          |  3 +-
 src/include/catalog/pg_publication_rel.h      |  1 +
 src/include/nodes/parsenodes.h                |  1 +
 src/test/regress/expected/publication.out     | 87 ++++++++++++++++-
 src/test/regress/sql/publication.sql          | 41 +++++++-
 .../t/033_rep_changes_skip_table.pl           | 96 +++++++++++++++++++
 23 files changed, 481 insertions(+), 99 deletions(-)
 create mode 100644 src/test/subscription/t/033_rep_changes_skip_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1b02af1c03..fb1446cb07 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6354,6 +6354,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        Reference to schema
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pnskip</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the schema is skip schema
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -6441,10 +6450,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
 
     <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>pnskip</structfield> <type>bool</type>
+       <structfield>prskip</structfield> <type>bool</type>
       </para>
       <para>
-       True if the schema is skip schema
+       True if the table is skip table
       </para></entry>
      </row>
     </tbody>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index e2a4b89226..e16d4b2f86 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -598,11 +598,11 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema or skip all tables in schema to a
-   publication, the user must be a superuser. To create a publication that
-   publishes all tables or all tables in schema automatically, the user must be
-   a superuser.
+   To add tables or skip tables to a publication, the user must have ownership
+   rights on the table. To add all tables in schema or skip all tables in
+   schema to a publication, the user must be a superuser. To create a
+   publication that publishes all tables or all tables in schema automatically,
+   the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 6f915d8c5d..f87a46d1e6 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
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    [SKIP] TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     [SKIP] ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -70,8 +70,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD [SKIP] ALL TABLES IN SCHEMA</literal> and
+   Adding a table or a skip table to a publication additionally requires owning
+   that table. The <literal>ADD [SKIP] ALL TABLES IN SCHEMA</literal> and
    <literal>SET [SKIP] ALL TABLES IN SCHEMA</literal> to a publication requires
    the invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
@@ -90,10 +90,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The <literal>ADD SKIP ALL TABLES IN SCHEMA</literal> and
-   <literal>SET SKIP ALL TABLES IN SCHEMA</literal> can be specified only for
+   Adding/Dropping/Setting <literal>SKIP ALL TABLES IN SCHEMA</literal>, and
+   <literal>SKIP TABLE</literal> can be specified only for
    <literal>FOR ALL TABLES</literal> publication. It is not supported for
-   <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+   <literal>FOR ALL TABLES IN SCHEMA </literal> publication,
+   <literal>FOR ALL SEQUENCES IN SCHEMA </literal> publication and
    <literal>FOR TABLE</literal> publication.
   </para>
  </refsect1>
@@ -232,6 +233,15 @@ ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLE
    <structname>mypublication</structname>:
 <programlisting>
 ALTER PUBLICATION mypublication ADD SKIP ALL TABLES IN SCHEMA sales_june, sales_july;
+</programlisting>
+  </para>
+
+   <para>
+   Add skip tables <structname>users</structname> and
+   <structname>departments</structname> to the publication
+   <structname>mypublication</structname>:
+<programlisting>
+ALTER PUBLICATION mypublication ADD SKIP TABLE users, departments;
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 5acf80452a..43a1a1d718 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA }]
+    [ FOR ALL TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA }] [SKIP TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -124,6 +124,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
    <varlistentry>
     <term><literal>SKIP ALL TABLES IN SCHEMA</literal></term>
     <listitem>
@@ -141,6 +142,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
+   <varlistentry>
+    <term><literal>SKIP TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that skips replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>SKIP TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>FOR ALL TABLES IN SCHEMA</literal></term>
     <listitem>
@@ -360,6 +379,14 @@ CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
    <structname>marketing</structname> and <structname>sales</structname>:
 <programlisting>
 CREATE PUBLICATION mypublication FOR ALL TABLE SKIP ALL TABLES IN SCHEMA marketing, sales;
+</programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table;
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE SKIP TABLE users, departments;
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index b097b863cd..d2ae2f757f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1869,8 +1869,8 @@ testdb=&gt;
         specified, only those publications whose names match the pattern are
         listed.
         If <literal>+</literal> is appended to the command name, the tables,
-        schemas and the skip schema associated with each publication are shown
-        as well.
+        the skip tables, schemas and the skip schema associated with each
+        publication are shown as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 3d2cab47a6..291e9746c9 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -303,9 +303,10 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = GetRelationPublications(ancestor, false);
 		List	   *aschemaPubids = NIL;
 		List       *askipschemaPubids = NIL;
+		List	   *askippubids;
 
 		level++;
 
@@ -320,9 +321,11 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 		{
 			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor), false);
 			askipschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor), true);
+			askippubids = GetRelationPublications(ancestor, true);
 
 			if (list_member_oid(aschemaPubids, puboid) ||
-				(puballtables && !list_member_oid(askipschemaPubids, puboid)))
+				(puballtables && !list_member_oid(askipschemaPubids, puboid) &&
+				 !list_member_oid(askippubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -401,6 +404,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prskip - 1] =
+		BoolGetDatum(pri->skip);
+
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -674,7 +680,7 @@ publication_add_schema(Oid pubid, PublicationSchInfo *pubsch, bool if_not_exists
 
 /* Gets list of publication oids for a relation */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bskip)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -688,7 +694,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (bskip == ((Form_pg_publication_rel) GETSTRUCT(tup))->prskip)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -728,6 +735,7 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+
 		result = GetPubPartitionOptionRelations(result, pub_partopt,
 												pubrel->prrelid);
 	}
@@ -794,8 +802,15 @@ GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
-	List	   *skipschemaidlist = NIL;
+
+	/*
+	 * pg_publication_rel and pg_publication_namespace  will only have skip
+	 * tables and skip namespaces in case of all tables publication, no need
+	 * to pass skip flag to get the relations.
+	 */
 	List	   *pubschemalist = GetPublicationSchemas(pubid);
+	List	   *skippubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+	List	   *skipschemaidlist = NIL;
 	ListCell   *cell;
 
 	foreach(cell, pubschemalist)
@@ -822,7 +837,8 @@ GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 
 		if (is_publishable_class(relid, relForm) &&
 			!(relForm->relispartition && pubviaroot) &&
-			!list_member_oid(skipschemaidlist, schid))
+			!list_member_oid(skipschemaidlist, schid) &&
+			!list_member_oid(skippubtablelist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -845,7 +861,8 @@ GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 
 			if (is_publishable_class(relid, relForm) &&
 				!relForm->relispartition &&
-				!list_member_oid(skipschemaidlist, schid))
+				!list_member_oid(skipschemaidlist, schid) &&
+				!list_member_oid(skippubtablelist, relid))
 				result = lappend_oid(result, relid);
 		}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6863900bd9..b26e606c7e 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -849,37 +849,32 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
 	{
-		Assert(!relations);
-
 		/* Invalidate relcache so that publication info is rebuilt. */
 		CacheInvalidateRelcacheAll();
 	}
-	else
-	{
 
-		/* FOR [SKIP] ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR [SKIP] ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+												PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-								   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
 	}
 
 	/* tables added through a schema */
@@ -1371,6 +1366,8 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 
 	bool		nonskipschema = false;
 	bool		skipschema = false;
+	bool		nonskiptable = false;
+	bool		skiptable = false;
 
 	foreach(lc, schemaidlist)
 	{
@@ -1382,6 +1379,16 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 			skipschema = true;
 	}
 
+	foreach(lc, tables)
+	{
+		PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+		if (!pub_table->skip)
+			nonskiptable = true;
+		else
+			skiptable = true;
+	}
+
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
 		ereport(ERROR,
@@ -1407,12 +1414,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 				errdetail("Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
 
 	/* Check that user is allowed to manipulate the publication tables. */
-	if (tables && pubform->puballtables)
+	if (nonskiptable && tables && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
+
+	if (skiptable && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+					   NameStr(pubform->pubname)),
+				errdetail("Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
 }
 
 /*
@@ -1689,6 +1703,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->skip = t->skip;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1761,6 +1776,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->skip = t->skip;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 141a2eabf8..f9655c140f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16276,7 +16276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16413,7 +16413,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid, false);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7fd8d194ce..be3d49f607 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -498,7 +498,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>	opt_interval interval_second
 %type <str>		unicode_normal_form
 
-%type <boolean> opt_instead
+%type <boolean> opt_instead opt_skip
 %type <boolean> opt_unique opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_analyze opt_default opt_recheck
 %type <defelt>	opt_binary copy_delimiter
@@ -9923,14 +9923,16 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			opt_skip TABLE relation_expr opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
-					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->skip = $1;
+					$$->pubtable->skip = $1;
+					$$->pubtable->relation = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10024,6 +10026,17 @@ pub_obj_list: 	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ skip_pub_obj_list:	pub_obj_list
+					{ $$ = $1; }
+			| /*EMPTY*/
+					{ $$ = NULL; }
+	;
+
+opt_skip:
+			SKIP									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10285,13 +10298,6 @@ opt_instead:
 			| /*EMPTY*/								{ $$ = false; }
 		;
 
-
- skip_pub_obj_list:	pub_obj_list
-						{ $$ = $1; }
-					| /*EMPTY*/
-						{ $$ = NULL; }
-	;
-
 /*****************************************************************************
  *
  *		QUERY:
@@ -18789,6 +18795,8 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 				pubobj->pubtable = pubtable;
 				pubobj->name = NULL;
 			}
+
+			pubobj->pubtable->skip = pubobj->skip;
 		}
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA)
@@ -18846,11 +18854,10 @@ preprocess_alltables_pubobj_list(List *pubobjspec_list, int location,
 		PublicationObjSpec *pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
-		if (pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
-			!pubobj->skip)
+		if (!pubobj->skip)
 			ereport(ERROR,
 					errcode(ERRCODE_SYNTAX_ERROR),
-					errmsg("only SKIP ALL TABLES IN SCHEMA can be specified with ALL TABLES option"),
+					errmsg("only SKIP ALL TABLES IN SCHEMA or SKIP TABLE can be specified with ALL TABLES option"),
 					parser_errposition(pubobj->location));
 	}
 }
@@ -18872,12 +18879,12 @@ check_skip_in_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	{
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
-		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
-		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA &&
-			pubobj->skip)
+		/* SKIP ALL TABLES IN SCHEMA/SKIP TABLE option supported only with ALL TABLES */
+		if ((pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
+			 pubobj->pubobjtype == PUBLICATIONOBJ_TABLE) && pubobj->skip)
 			ereport(ERROR,
 					errcode(ERRCODE_SYNTAX_ERROR),
-					errmsg("SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option"),
+					errmsg("SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option"),
 					parser_errposition(pubobj->location));
 	}
 }
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 19181297af..b81ac797b8 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1989,7 +1989,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -1998,6 +1998,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		 */
 		List	   *schemaPubids = GetSchemaPublications(schemaId, false);
 		List       *skipSchemaPubids = GetSchemaPublications(schemaId, true);
+		List	   *skipTablePubids = GetRelationPublications(relid, true);
 		ListCell   *lc;
 		Oid			publish_as_relid = relid;
 		int			publish_ancestor_level = 0;
@@ -2113,7 +2114,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
-					(pub->alltables && !list_member_oid(skipSchemaPubids, pub->oid)) ||
+					(pub->alltables &&
+					 !list_member_oid(skipSchemaPubids, pub->oid) &&
+					 !list_member_oid(skipTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2190,6 +2193,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		list_free(pubids);
 		list_free(schemaPubids);
 		list_free(skipSchemaPubids);
+		list_free(skipTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 02c0a47ca1..5b51185656 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5563,7 +5563,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
 	List	   *alltablespuboids;
-	List	   *skipschemapuboids;
+	List	   *skippuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5597,10 +5597,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid, false));
-	skipschemapuboids = GetSchemaPublications(schemaid, true);
+	skippuboids = GetSchemaPublications(schemaid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5612,20 +5612,22 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid, false));
-			skipschemapuboids = list_concat_unique_oid(skipschemapuboids,
-													   GetSchemaPublications(schemaid,
-																			 true));
+			skippuboids = list_concat_unique_oid(skippuboids,
+												 GetSchemaPublications(schemaid,
+																	   true));
+			skippuboids = list_concat_unique_oid(skippuboids,
+												 GetRelationPublications(ancestor, true));
 		}
 	}
 
 	alltablespuboids = GetAllTablesPublications();
 	puboids = list_concat_unique_oid(puboids,
 									 list_difference_oid(alltablespuboids,
-														 skipschemapuboids));
+														 skippuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 296527bde5..ee57de9890 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -282,7 +282,8 @@ static void dumpBlob(Archive *fout, const BlobInfo *binfo);
 static int	dumpBlobs(Archive *fout, const void *arg);
 static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
-static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
+static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+								 bool bskip);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
@@ -4099,6 +4100,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prskip;
 	int			i,
 				j,
 				ntups;
@@ -4111,7 +4113,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
+							 "SELECT tableoid, oid, prpubid, prrelid, prskip,"
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4137,6 +4139,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prskip = PQfnumber(res, "prskip");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4148,6 +4151,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char       *prskip = pg_strdup(PQgetvalue(res, i, i_prskip));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4168,7 +4172,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prskip, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_SKIP_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4267,13 +4275,15 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo,
  *	  dump the definition of the given publication table mapping
  */
 static void
-dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
+dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+					 bool bskip)
 {
 	DumpOptions *dopt = fout->dopt;
 	PublicationInfo *pubinfo = pubrinfo->publication;
 	TableInfo  *tbinfo = pubrinfo->pubtable;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4283,8 +4293,15 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
 					  fmtId(pubinfo->dobj.name));
+
+	if (bskip)
+		appendPQExpBufferStr(query, "SKIP ");
+
+	appendPQExpBufferStr(query, "TABLE ONLY");
+	description = (bskip) ? "SKIP PUBLICATION TABLE" : "PUBLICATION TABLE";
+
 	appendPQExpBuffer(query, " %s",
 					  fmtQualifiedDumpable(tbinfo));
 
@@ -4313,7 +4330,7 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLE",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9889,7 +9906,10 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
 		case DO_PUBLICATION_REL:
-			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, false);
+			break;
+		case DO_PUBLICATION_SKIP_REL:
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, true);
 			break;
 		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
@@ -17828,6 +17848,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
+			case DO_PUBLICATION_SKIP_REL:
 			case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cb9e5e164b..7dbd0f6538 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_POLICY,
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
+	DO_PUBLICATION_SKIP_REL,
 	DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 47d4baecb3..4336f4f674 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -91,6 +91,7 @@ enum dbObjectTypePriorities
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
 	PRIO_PUBLICATION_REL,
+	PRIO_PUBLICATION_SKIP_REL,
 	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -146,6 +147,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
+	PRIO_PUBLICATION_SKIP_REL,	/* DO_PUBLICATION_SKIP_REL */
 	PRIO_PUBLICATION_SKIP_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1490,6 +1492,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION TABLE (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_SKIP_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION SKIP TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_SKIP_TABLE_IN_SCHEMA:
 			snprintf(buf, bufsize,
 					 "PUBLICATION SKIP TABLES IN SCHEMA (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 2dbb43c30e..b902cca44d 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2463,6 +2463,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2588,6 +2597,20 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'ALTER PUBLICATION pub6 ADD SKIP TABLE test_table' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub6 ADD SKIP TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub6 ADD SKIP TABLE ONLY dump_test.test_table;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd6b63e7e2..3d61317e9a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2911,7 +2911,7 @@ describeOneTableDetails(const char *schemaname,
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "WHERE pr.prrelid = '%s' AND pr.prskip = 'f'\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
@@ -2923,8 +2923,13 @@ describeOneTableDetails(const char *schemaname,
 								  "								JOIN pg_catalog.pg_class pc\n"
 								  "	  	 						ON pc.relnamespace = pn.pnnspid\n"
 								  "							WHERE pc.oid ='%s' AND pn.pnpubid = p.oid)\n"
+								  "		AND NOT EXISTS (SELECT 1\n"
+								  "							FROM pg_catalog.pg_publication_rel pr\n"
+								  "								JOIN pg_catalog.pg_class pc\n"
+								  "	  	 						ON pr.prrelid = pc.oid\n"
+								  "							WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
 								  "ORDER BY 1;",
-								  oid, oid, oid, oid, oid);
+								  oid, oid, oid, oid, oid, oid);
 			}
 			else
 			{
@@ -6159,6 +6164,7 @@ describePublications(const char *pattern)
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n"
+							  "  AND pr.prskip = 'f'\n"
 							  "ORDER BY 1,2", pubid);
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
@@ -6191,6 +6197,18 @@ describePublications(const char *pattern)
 			if (!addFooterToPublicationDesc(&buf, "Skip tables from schemas:",
 											true, &cont))
 				goto error_return;
+
+			/* Get the skip tables for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+							  "FROM pg_catalog.pg_class c\n"
+							  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+							  "WHERE pr.prpubid = '%s'\n"
+							  "  AND pr.prskip = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Skip tables:",
+											true, &cont))
+				goto error_return;
 		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 402ab90b58..7d4c3ef405 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1828,10 +1828,11 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "SKIP TABLE", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "SKIP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "SKIP", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
@@ -1853,14 +1854,14 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "SKIP TABLE", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP", "SKIP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "SKIP ALL TABLES IN SCHEMA", "SKIP TABLE", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "SKIP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA") ||
 			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
@@ -2998,7 +2999,9 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (", "SKIP ALL TABLES IN SCHEMA");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "SKIP ALL TABLES IN SCHEMA", "SKIP TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "SKIP"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA",  "TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 30a2fcb974..12fca8ea2b 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,6 +108,7 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		skip;
 } PublicationRelInfo;
 
 typedef struct PublicationSchInfo
@@ -119,7 +120,7 @@ typedef struct PublicationSchInfo
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool bskip);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 4feb581899..ab6efc5b30 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prskip	BKI_DEFAULT(f);				/* skip the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 188285c99d..86372a1531 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4003,6 +4003,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		skip;			/* skip relation */
 } PublicationTable;
 
 /*
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e7e46d0841..0a36ec06b9 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -110,6 +110,35 @@ ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA public;
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+-- should be able to add skip table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Skip tables:
+    "public.testpub_tbl1"
+
+-- should be able to set skip table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Skip tables:
+    "public.testpub_tbl2"
+
+-- should be able to drop skip table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+(1 row)
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -157,6 +186,18 @@ DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON
 ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
 ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
 DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't add skip table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -194,6 +235,18 @@ DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON
 ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
 ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
 DETAIL:  Skip tables from schema cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't add skip table to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop skip table from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set skip table to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  Skip table cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -231,21 +284,47 @@ Skip tables from schemas:
 
 -- fail - can't specify skip schema along with table publication
 CREATE PUBLICATION testpub_fortable_skipschema FOR TABLE pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
-ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+ERROR:  SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option
 LINE 1: ...E pub_test.testpub_nopk, SKIP ALL TABLES IN SCHEMA pub_test;
                                                               ^
 -- fail - can't specify skip schema along with schema publication
 CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
-ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+ERROR:  SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option
 LINE 1: ...BLES IN SCHEMA pub_test, SKIP ALL TABLES IN SCHEMA pub_test;
                                                               ^
 -- fail - can't specify only skip schema while create publication
 CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
-ERROR:  SKIP ALL TABLES IN SCHEMA can be specified only with ALL TABLES option
+ERROR:  SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option
 LINE 1: ...N testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
                                                               ^
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skiptable FOR ALL TABLES SKIP TABLE testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_skiptable
+                         Publication testpub_foralltables_skiptable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Skip tables:
+    "public.testpub_tbl1"
+
+-- fail - can't specify skip table along with table publication
+CREATE PUBLICATION testpub_fortable_skiptable FOR TABLE pub_test.testpub_nopk, SKIP TABLE testpub_tbl1;
+ERROR:  SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option
+LINE 1: CREATE PUBLICATION testpub_fortable_skiptable FOR TABLE pub_...
+        ^
+-- fail - can't specify skip table along with schema publication
+CREATE PUBLICATION testpub_fortable_skiptable FOR ALL TABLES IN SCHEMA pub_test, SKIP TABLE testpub_tbl1;
+ERROR:  SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option
+LINE 1: CREATE PUBLICATION testpub_fortable_skiptable FOR ALL TABLES...
+        ^
+-- fail - can't specify only skip table while create publication
+CREATE PUBLICATION testpub_fortable_skiptable FOR SKIP TABLE testpub_tbl1;
+ERROR:  SKIP ALL TABLES IN SCHEMA/SKIP TABLE can be specified only with ALL TABLES option
+LINE 1: CREATE PUBLICATION testpub_fortable_skiptable FOR SKIP TABLE...
+        ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema, testpub_foralltables_skiptable;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 8d8a522c76..4a1d2f7013 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -68,6 +68,16 @@ ALTER PUBLICATION testpub_foralltables SET SKIP ALL TABLES IN SCHEMA public;
 ALTER PUBLICATION testpub_foralltables DROP SKIP ALL TABLES IN SCHEMA public;
 \dRp+ testpub_foralltables
 
+-- should be able to add skip table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD SKIP TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+-- should be able to set skip table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET SKIP TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+-- should be able to drop skip table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP SKIP TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 RESET client_min_messages;
@@ -88,6 +98,13 @@ ALTER PUBLICATION testpub_fortable DROP SKIP ALL TABLES IN SCHEMA pub_test;
 -- fail - can't set skip schema to 'FOR TABLE' publication
 ALTER PUBLICATION testpub_fortable SET SKIP ALL TABLES IN SCHEMA pub_test;
 
+-- fail - can't add skip table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD SKIP TABLE testpub_tbl1;
+-- fail - can't drop skip table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP SKIP TABLE testpub_tbl1;
+-- fail - can't set skip table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET SKIP TABLE testpub_tbl1;
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -109,6 +126,13 @@ ALTER PUBLICATION testpub_forschema DROP SKIP ALL TABLES IN SCHEMA pub_test;
 -- fail - can't set skip schema to schema  publication
 ALTER PUBLICATION testpub_forschema SET SKIP ALL TABLES IN SCHEMA pub_test;
 
+-- fail - can't add skip table to schema publication
+ALTER PUBLICATION testpub_forschema ADD SKIP TABLE testpub_tbl1;
+-- fail - can't drop skip table from schema publication
+ALTER PUBLICATION testpub_forschema DROP SKIP TABLE testpub_tbl1;
+-- fail - can't set skip table to schema  publication
+ALTER PUBLICATION testpub_forschema SET SKIP TABLE testpub_tbl1;
+
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
@@ -128,8 +152,23 @@ CREATE PUBLICATION testpub_forschema_skipschema FOR ALL TABLES IN SCHEMA pub_tes
 -- fail - can't specify only skip schema while create publication
 CREATE PUBLICATION testpub_skipschema FOR SKIP ALL TABLES IN SCHEMA pub_test;
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_skiptable FOR ALL TABLES SKIP TABLE testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_skiptable
+
+-- fail - can't specify skip table along with table publication
+CREATE PUBLICATION testpub_fortable_skiptable FOR TABLE pub_test.testpub_nopk, SKIP TABLE testpub_tbl1;
+
+-- fail - can't specify skip table along with schema publication
+CREATE PUBLICATION testpub_fortable_skiptable FOR ALL TABLES IN SCHEMA pub_test, SKIP TABLE testpub_tbl1;
+
+-- fail - can't specify only skip table while create publication
+CREATE PUBLICATION testpub_fortable_skiptable FOR SKIP TABLE testpub_tbl1;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_skipschema, testpub_foralltables_skiptable;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/033_rep_changes_skip_table.pl b/src/test/subscription/t/033_rep_changes_skip_table.pl
new file mode 100644
index 0000000000..6c1dc6f382
--- /dev/null
+++ b/src/test/subscription/t/033_rep_changes_skip_table.pl
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for skip table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES SKIP TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES SKIP TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the schema table data does not sync for skip schemas
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is skipped for skip schemas');
+
+# Insert some data into few tables and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to skip data changes in public.tab1 and verify that subscriber does not get
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add SKIP TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop skip table public.tab1 and verify that subscriber gets
+# the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop SKIP TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#4Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#3)
Re: Skipping schema changes in publication

On Tue, Apr 12, 2022 at 11:53 AM vignesh C <vignesh21@gmail.com> wrote:

On Sat, Mar 26, 2022 at 7:37 PM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Mar 22, 2022 at 12:38 PM vignesh C <vignesh21@gmail.com> wrote:

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.

The patch was not applying on top of HEAD because of the recent
commits, attached patch is rebased on top of HEAD.

The patch does not apply on top of HEAD because of the recent commit,
attached patch is rebased on top of HEAD.

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

For the second syntax (Alter Publication ...), isn't it better to
avoid using ADD? It looks odd to me because we are not adding anything
in publication with this sytax.

--
With Regards,
Amit Kapila.

#5vignesh C
vignesh21@gmail.com
In reply to: Amit Kapila (#4)
Re: Skipping schema changes in publication

On Tue, Apr 12, 2022 at 12:19 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Tue, Apr 12, 2022 at 11:53 AM vignesh C <vignesh21@gmail.com> wrote:

On Sat, Mar 26, 2022 at 7:37 PM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Mar 22, 2022 at 12:38 PM vignesh C <vignesh21@gmail.com> wrote:

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.

The patch was not applying on top of HEAD because of the recent
commits, attached patch is rebased on top of HEAD.

The patch does not apply on top of HEAD because of the recent commit,
attached patch is rebased on top of HEAD.

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

For the second syntax (Alter Publication ...), isn't it better to
avoid using ADD? It looks odd to me because we are not adding anything
in publication with this sytax.

I was thinking of the scenario where user initially creates the
publication for all tables:
CREATE PUBLICATION pub1 FOR ALL TABLES;

After that user decides to skip few tables ex: t1, t2
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

I thought of supporting this syntax if incase user decides to add the
skipping of a few tables later.
Thoughts?

Regards,
Vignesh

#6Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#5)
Re: Skipping schema changes in publication

On Tue, Apr 12, 2022 at 4:17 PM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Apr 12, 2022 at 12:19 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

For the second syntax (Alter Publication ...), isn't it better to
avoid using ADD? It looks odd to me because we are not adding anything
in publication with this sytax.

I was thinking of the scenario where user initially creates the
publication for all tables:
CREATE PUBLICATION pub1 FOR ALL TABLES;

After that user decides to skip few tables ex: t1, t2
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

I thought of supporting this syntax if incase user decides to add the
skipping of a few tables later.

I understand that part but what I pointed out was that it might be
better to avoid using ADD keyword in this syntax like: ALTER
PUBLICATION pub1 SKIP TABLE t1,t2;

--
With Regards,
Amit Kapila.

#7vignesh C
vignesh21@gmail.com
In reply to: Amit Kapila (#6)
Re: Skipping schema changes in publication

On Tue, Apr 12, 2022 at 4:46 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Tue, Apr 12, 2022 at 4:17 PM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Apr 12, 2022 at 12:19 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

For the second syntax (Alter Publication ...), isn't it better to
avoid using ADD? It looks odd to me because we are not adding anything
in publication with this sytax.

I was thinking of the scenario where user initially creates the
publication for all tables:
CREATE PUBLICATION pub1 FOR ALL TABLES;

After that user decides to skip few tables ex: t1, t2
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

I thought of supporting this syntax if incase user decides to add the
skipping of a few tables later.

I understand that part but what I pointed out was that it might be
better to avoid using ADD keyword in this syntax like: ALTER
PUBLICATION pub1 SKIP TABLE t1,t2;

Currently we are supporting Alter publication using the following syntax:
ALTER PUBLICATION pub1 ADD TABLE t1;
ALTER PUBLICATION pub1 SET TABLE t1;
ALTER PUBLICATION pub1 DROP TABLE T1;
ALTER PUBLICATION pub1 ADD ALL TABLES IN SCHEMA sch1;
ALTER PUBLICATION pub1 SET ALL TABLES IN SCHEMA sch1;
ALTER PUBLICATION pub1 DROP ALL TABLES IN SCHEMA sch1;

I have extended the new syntax in similar lines:
ALTER PUBLICATION pub1 ADD SKIP TABLE t1;
ALTER PUBLICATION pub1 SET SKIP TABLE t1;
ALTER PUBLICATION pub1 DROP SKIP TABLE T1;

I did it like this to maintain consistency.
But I'm fine doing it either way to keep it simple for the user.

Regards,
Vignesh

#8Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#7)
Re: Skipping schema changes in publication

On Wed, Apr 13, 2022 at 8:45 AM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Apr 12, 2022 at 4:46 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

I understand that part but what I pointed out was that it might be
better to avoid using ADD keyword in this syntax like: ALTER
PUBLICATION pub1 SKIP TABLE t1,t2;

Currently we are supporting Alter publication using the following syntax:
ALTER PUBLICATION pub1 ADD TABLE t1;
ALTER PUBLICATION pub1 SET TABLE t1;
ALTER PUBLICATION pub1 DROP TABLE T1;
ALTER PUBLICATION pub1 ADD ALL TABLES IN SCHEMA sch1;
ALTER PUBLICATION pub1 SET ALL TABLES IN SCHEMA sch1;
ALTER PUBLICATION pub1 DROP ALL TABLES IN SCHEMA sch1;

I have extended the new syntax in similar lines:
ALTER PUBLICATION pub1 ADD SKIP TABLE t1;
ALTER PUBLICATION pub1 SET SKIP TABLE t1;
ALTER PUBLICATION pub1 DROP SKIP TABLE T1;

I did it like this to maintain consistency.

What is the difference between ADD and SET variants? I understand we
need some way to remove the SKIP table setting but not sure if DROP is
the best alternative.

The other ideas could be:
To set skip tables: ALTER PUBLICATION pub1 SKIP TABLE t1, t2...;
To reset skip tables: ALTER PUBLICATION pub1 SKIP TABLE; /* basically
an empty list*/
Yet another way to reset skip tables: ALTER PUBLICATION pub1 RESET
SKIP TABLE; /* Here we need to introduce RESET. */

--
With Regards,
Amit Kapila.

#9Peter Smith
smithpb2250@gmail.com
In reply to: Amit Kapila (#8)
Re: Skipping schema changes in publication

On Wed, Apr 13, 2022 at 2:40 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Apr 13, 2022 at 8:45 AM vignesh C <vignesh21@gmail.com> wrote:

On Tue, Apr 12, 2022 at 4:46 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

I understand that part but what I pointed out was that it might be
better to avoid using ADD keyword in this syntax like: ALTER
PUBLICATION pub1 SKIP TABLE t1,t2;

Currently we are supporting Alter publication using the following syntax:
ALTER PUBLICATION pub1 ADD TABLE t1;
ALTER PUBLICATION pub1 SET TABLE t1;
ALTER PUBLICATION pub1 DROP TABLE T1;
ALTER PUBLICATION pub1 ADD ALL TABLES IN SCHEMA sch1;
ALTER PUBLICATION pub1 SET ALL TABLES IN SCHEMA sch1;
ALTER PUBLICATION pub1 DROP ALL TABLES IN SCHEMA sch1;

I have extended the new syntax in similar lines:
ALTER PUBLICATION pub1 ADD SKIP TABLE t1;
ALTER PUBLICATION pub1 SET SKIP TABLE t1;
ALTER PUBLICATION pub1 DROP SKIP TABLE T1;

I did it like this to maintain consistency.

What is the difference between ADD and SET variants? I understand we
need some way to remove the SKIP table setting but not sure if DROP is
the best alternative.

The other ideas could be:
To set skip tables: ALTER PUBLICATION pub1 SKIP TABLE t1, t2...;
To reset skip tables: ALTER PUBLICATION pub1 SKIP TABLE; /* basically
an empty list*/
Yet another way to reset skip tables: ALTER PUBLICATION pub1 RESET
SKIP TABLE; /* Here we need to introduce RESET. */

When you were talking about SKIP TABLE then I liked the idea of:

ALTER ... SET SKIP TABLE; /* empty list to reset the table skips */
ALTER ... SET SKIP TABLE t1,t2; /* non-empty list to replace the table skips */

But when you apply that rule to SKIP ALL TABLES IN SCHEMA, then the
reset syntax looks too awkward.

ALTER ... SET SKIP ALL TABLES IN SCHEMA; /* empty list to reset the
schema skips */
ALTER ... SET SKIP ALL TABLES IN SCHEMA s1,s2; /* non-empty list to
replace the schema skips */

~~~

IMO it might be simpler to do it like:

ALTER ... DROP SKIP; /* reset/remove the skip */
ALTER ... SET SKIP TABLE t1,t2; /* non-empty list to replace table skips */
ALTER ... SET SKIP ALL TABLES IS SCHEMA s1,s2; /* non-empty list to
replace schema skips */

I don't really think that the ALTER ... SET SKIP empty list should be
supported (because reason above)
I don't really think that the ALTER ... ADD SKIP should be supported.

===

More questions - What happens if the skip table or skip schema no
longer exists exist? Does that mean error? Maybe there is a
dependency on it but OTOH it might be annoying - e.g. to disallow a
DROP TABLE when the only dependency was that the user wanted to SKIP
it...

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

#10shiy.fnst@fujitsu.com
shiy.fnst@fujitsu.com
In reply to: vignesh C (#3)
RE: Skipping schema changes in publication

On Tue, Apr 12, 2022 2:23 PM vignesh C <vignesh21@gmail.com> wrote:

The patch does not apply on top of HEAD because of the recent commit,
attached patch is rebased on top of HEAD.

Thanks for your patch. Here are some comments for 0001 patch.

1. doc/src/sgml/catalogs.sgml
@@ -6438,6 +6438,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        A null value indicates that all columns are published.
       </para></entry>
      </row>
+
+    <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pnskip</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the schema is skip schema
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>

This change is added to pg_publication_rel, I think it should be added to
pg_publication_namespace, right?

2.
postgres=# alter publication p1 add skip all tables in schema s1,s2;
ERROR: schema "s1" is already member of publication "p1"

This error message seems odd to me, can we improve it? Something like:
schema "s1" is already skipped in publication "p1"

3.
create table tbl (a int primary key);
create schema s1;
create schema s2;
create table s1.tbl (a int);
create publication p1 for all tables skip all tables in schema s1,s2;

postgres=# \dRp+
Publication p1
Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
----------+------------+---------+---------+---------+-----------+----------
postgres | t | t | t | t | t | f
Skip tables from schemas:
"s1"
"s2"

postgres=# select * from pg_publication_tables;
pubname | schemaname | tablename
---------+------------+-----------
p1 | public | tbl
p1 | s1 | tbl
(2 rows)

There shouldn't be a record of s1.tbl, since all tables in schema s1 are skipped.

I found that it is caused by the following code:

src/backend/catalog/pg_publication.c
+	foreach(cell, pubschemalist)
+	{
+		PublicationSchInfo *pubsch = (PublicationSchInfo *) lfirst(cell);
+
+		skipschemaidlist = lappend_oid(result, pubsch->oid);
+	}

The first argument to append_oid() seems wrong, should it be:

skipschemaidlist = lappend_oid(skipschemaidlist, pubsch->oid);

4. src/backend/commands/publicationcmds.c

/*
* Convert the PublicationObjSpecType list into schema oid list and
* PublicationTable list.
*/
static void
ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
List **rels, List **schemas)

Should we modify the comment of ObjectsInPublicationToOids()?
"schema oid list" should be "PublicationSchInfo list".

Regards,
Shi yu

#11wangw.fnst@fujitsu.com
wangw.fnst@fujitsu.com
In reply to: vignesh C (#3)
RE: Skipping schema changes in publication

On Tue, Apr 12, 2022 at 2:23 PM vignesh C <vignesh21@gmail.com> wrote:

The patch does not apply on top of HEAD because of the recent commit,
attached patch is rebased on top of HEAD.

Thanks for your patches.

Here are some comments for v1-0001:
1.
I found the patch add the following two new functions in gram.y:
preprocess_alltables_pubobj_list, check_skip_in_pubobj_list.
These two functions look similar. So could we just add one new function?
Besides, do we need the API `location` in new function
preprocess_alltables_pubobj_list? It seems that "location" is not used in this
new function.
In addition, the location of error cursor in the messages seems has a little
problem. For example:
postgres=# create publication pub for all TABLES skip all tables in schema public, table test;
ERROR: only SKIP ALL TABLES IN SCHEMA or SKIP TABLE can be specified with ALL TABLES option
LINE 1: create publication pub for all TABLES skip all tables in sch...
^
(The location of error cursor is under 'create')

2. I think maybe there is a minor missing in function
preprocess_alltables_pubobj_list and check_skip_in_pubobj_list:
We seem to be missing the CURRENT_SCHEMA case.
For example(In function preprocess_alltables_pubobj_list) :
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if (pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
+			!pubobj->skip)
maybe need to be changed like this:
+		/* Only SKIP ALL TABLES IN SCHEMA option supported with ALL TABLES */
+		if ((pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_SCHEMA &&
+		    pubobj->pubobjtype != PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA) &&
+			pubobj->skip)
3. I think maybe there are some minor missing in create_publication.sgml.
+    [ FOR ALL TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA }]
maybe need to be changed to this:
+    [ FOR ALL TABLES [SKIP ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]]
4. The error message of function CreatePublication.
Does the message below need to be modified like the comment?
In addition, I think maybe "FOR/SKIP" is better.
@@ -835,18 +843,21 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
+		/* FOR [SKIP] ALL TABLES IN SCHEMA requires superuser */
 		if (list_length(schemaidlist) > 0 && !superuser())
 			ereport(ERROR,
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
5.
I think there are some minor missing in tab-complete.c.
+			 Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
maybe need to be changed to this:
+			 Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "SKIP", "ALL", "TABLES", "IN", "SCHEMA"))
+			  Matches("CREATE", "PUBLICATION", MatchAny, "SKIP", "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny)) &&
maybe need to be changed to this:
+			  Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "SKIP", "ALL", "TABLES", "IN", "SCHEMA", MatchAny)) &&

6.
In function get_rel_sync_entry, do we need `if (!publish)` in below code?
I think `publish` is always false here, as we delete the check for
"pub->alltables".
```
- /*
- * If this is a FOR ALL TABLES publication, pick the partition root
- * and set the ancestor level accordingly.
- */
- if (pub->alltables)
- {
- ......
- }
-
if (!publish)
```

Regards,
Wang wei

#12Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: vignesh C (#3)
Re: Skipping schema changes in publication

On 12.04.22 08:23, vignesh C wrote:

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

We have already allocated the "skip" terminology for skipping
transactions, which is a dynamic run-time action. We are also using the
term "skip" elsewhere to skip locked rows, which is similarly a run-time
action. I think it would be confusing to use the term SKIP for DDL
construction.

Let's find another term like "omit", "except", etc.

I would also think about this in broader terms. For example, sometimes
people want features like "all columns except these" in certain places.
The syntax for those things should be similar.

That said, I'm not sure this feature is worth the trouble. If this is
useful, what about "whole database except these schemas"? What about
"create this database from this template except these schemas". This
could get out of hand. I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

#13Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#12)
Re: Skipping schema changes in publication

On Thu, Apr 14, 2022, at 10:47 AM, Peter Eisentraut wrote:

On 12.04.22 08:23, vignesh C wrote:

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

We have already allocated the "skip" terminology for skipping
transactions, which is a dynamic run-time action. We are also using the
term "skip" elsewhere to skip locked rows, which is similarly a run-time
action. I think it would be confusing to use the term SKIP for DDL
construction.

I didn't like the SKIP choice too. We already have EXCEPT for IMPORT FOREIGN
SCHEMA and if I were to suggest a keyword, it would be EXCEPT.

I would also think about this in broader terms. For example, sometimes
people want features like "all columns except these" in certain places.
The syntax for those things should be similar.

The questions are:
What kind of issues does it solve?
Do we have a workaround for it?

That said, I'm not sure this feature is worth the trouble. If this is
useful, what about "whole database except these schemas"? What about
"create this database from this template except these schemas". This
could get out of hand. I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

I have the same impression too. We already provide a way to:

* include individual tables;
* include all tables;
* include all tables in a certain schema.

Doesn't it cover the majority of the use cases? We don't need to cover all
possible cases in one DDL command. IMO the current grammar for CREATE
PUBLICATION is already complicated after the ALL TABLES IN SCHEMA. You are
proposing to add "ALL TABLES SKIP ALL TABLES" that sounds repetitive but it is
not; doesn't seem well-thought-out. I'm also concerned about possible gotchas
for this proposal. The first command above suggests that it skips all tables in a
certain schema. What happen if I decide to include a particular table of the
skipped schema (second command)?

ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;
ALTER PUBLICATION pub1 ADD TABLE s1.foo;

Having said that I'm not wedded to this proposal. Unless someone provides
compelling use cases for this additional syntax, I think we should leave the
publication syntax as is.

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

#14Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#13)
Re: Skipping schema changes in publication

On Fri, Apr 15, 2022 at 1:26 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Apr 14, 2022, at 10:47 AM, Peter Eisentraut wrote:

On 12.04.22 08:23, vignesh C wrote:

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

We have already allocated the "skip" terminology for skipping
transactions, which is a dynamic run-time action. We are also using the
term "skip" elsewhere to skip locked rows, which is similarly a run-time
action. I think it would be confusing to use the term SKIP for DDL
construction.

I didn't like the SKIP choice too. We already have EXCEPT for IMPORT FOREIGN
SCHEMA and if I were to suggest a keyword, it would be EXCEPT.

+1 for EXCEPT.

I would also think about this in broader terms. For example, sometimes
people want features like "all columns except these" in certain places.
The syntax for those things should be similar.

The questions are:
What kind of issues does it solve?

As far as I understand, it is for usability, otherwise, users need to
list all required columns' names even if they don't want to hide most
of the columns in the table. Consider user doesn't want to publish the
'salary' or other sensitive information of executives/employees but
would like to publish all other columns. I feel in such cases it will
be a lot of work for the user especially when the table has many
columns. I see that Oracle has a similar feature [1]https://docs.oracle.com/en/cloud/paas/goldengate-cloud/gwuad/selecting-columns.html#GUID-9A851C8B-48F7-43DF-8D98-D086BE069E20. I think without
this it will be difficult for users to use this feature in some cases.

Do we have a workaround for it?

I can't think of any except the user needs to manually input all
required columns. Can you think of any other workaround?

That said, I'm not sure this feature is worth the trouble. If this is
useful, what about "whole database except these schemas"? What about
"create this database from this template except these schemas". This
could get out of hand. I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

I have the same impression too. We already provide a way to:

* include individual tables;
* include all tables;
* include all tables in a certain schema.

Doesn't it cover the majority of the use cases?

Similar to columns, the same applies to tables. Users need to manually
add all tables for a database even when she wants to avoid only a
handful of tables from the database say because they contain sensitive
information or are not required. I think we don't need to cover all
possible exceptions but a few where users can avoid some tables would
be useful. If not, what kind of alternative do users have except for
listing all columns or all tables that are required.

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

--
With Regards,
Amit Kapila.

#15vignesh C
vignesh21@gmail.com
In reply to: Peter Eisentraut (#12)
Re: Skipping schema changes in publication

On Thu, Apr 14, 2022 at 7:18 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 12.04.22 08:23, vignesh C wrote:

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

We have already allocated the "skip" terminology for skipping
transactions, which is a dynamic run-time action. We are also using the
term "skip" elsewhere to skip locked rows, which is similarly a run-time
action. I think it would be confusing to use the term SKIP for DDL
construction.

Let's find another term like "omit", "except", etc.

+1 for Except

I would also think about this in broader terms. For example, sometimes
people want features like "all columns except these" in certain places.
The syntax for those things should be similar.

That said, I'm not sure this feature is worth the trouble. If this is
useful, what about "whole database except these schemas"? What about
"create this database from this template except these schemas". This
could get out of hand. I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

I thought this feature would help when there are many many tables in
the database and the user wants only certain confidential tables like
credit card information. In this case instead of specifying the whole
table list it will be better to specify "ALL TABLES EXCEPT
cred_info_tbl".
I had seen that mysql also has a similar option replicate-ignore-table
to ignore the changes on specific tables as mentioned in [1]https://dev.mysql.com/doc/refman/5.7/en/change-replication-filter.html.
Similar use case exists in pg_dump too. pg_dump has an option
exclude-table that will be used for not dumping any tables that are
matching the table specified as in [2]https://www.postgresql.org/docs/devel/app-pgdump.html.

[1]: https://dev.mysql.com/doc/refman/5.7/en/change-replication-filter.html
[2]: https://www.postgresql.org/docs/devel/app-pgdump.html

Regards,
Vignesh

#16vignesh C
vignesh21@gmail.com
In reply to: Amit Kapila (#14)
1 attachment(s)
Re: Skipping schema changes in publication

On Mon, Apr 18, 2022 at 12:32 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Fri, Apr 15, 2022 at 1:26 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Apr 14, 2022, at 10:47 AM, Peter Eisentraut wrote:

On 12.04.22 08:23, vignesh C wrote:

I have also included the implementation for skipping a few tables from
all tables publication, the 0002 patch has the implementation for the
same.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
tables.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD SKIP TABLE t1,t2;

We have already allocated the "skip" terminology for skipping
transactions, which is a dynamic run-time action. We are also using the
term "skip" elsewhere to skip locked rows, which is similarly a run-time
action. I think it would be confusing to use the term SKIP for DDL
construction.

I didn't like the SKIP choice too. We already have EXCEPT for IMPORT FOREIGN
SCHEMA and if I were to suggest a keyword, it would be EXCEPT.

+1 for EXCEPT.

Updated patch by changing the syntax to use EXCEPT instead of SKIP.

Regards,
Vignesh

Attachments:

v2-0001-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 289b5acfc33f70f488f45e2cb55714d20097ac4c Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Wed, 20 Apr 2022 11:19:50 +0530
Subject: [PATCH v2] Skip publishing the tables specified in EXCEPT TABLE.

A new option "EXCEPT TABLE" in Create/Alter Publication allows
one or more tables to be excluded, publisher will exclude sending the data
of the tables present in the except table to the subscriber.

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD EXCEPT TABLE t1,t2;

A new column prexcept is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude publishing through the publication.
Modified the output plugin (pgoutput) to exclude publishing the changes if the
relation is part of except table publication.

Updates pg_dump to identify and dump except table publications. Updates the \d
family of commands to display except table publications and \dRp+ variant will
now display associated except tables if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 ++
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  14 ++-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  36 ++++--
 src/backend/commands/publicationcmds.c        | 106 +++++++++++-------
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     | 102 +++++++++++++++--
 src/backend/replication/pgoutput/pgoutput.c   |  25 ++---
 src/backend/utils/cache/relcache.c            |  17 ++-
 src/bin/pg_dump/pg_dump.c                     |  35 ++++--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  23 ++++
 src/bin/psql/describe.c                       |  25 ++++-
 src/bin/psql/tab-complete.c                   |  15 ++-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   4 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  81 ++++++++++++-
 src/test/regress/sql/publication.sql          |  40 ++++++-
 .../t/033_rep_changes_except_table.pl         |  97 ++++++++++++++++
 24 files changed, 580 insertions(+), 113 deletions(-)
 create mode 100644 src/test/subscription/t/033_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a533a2153e..78e8c22a59 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the table must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index a0f9cecd01..36e943d305 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1160,10 +1160,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To add tables or exclude tables to a publication, the user must have
+   ownership rights on the table. To add all tables in schema to a publication,
+   the user must be a superuser. To create a publication that publishes all
+   tables or all tables in schema automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..2a8e4e041b 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
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    [EXCEPT] TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -70,8 +70,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
+   Adding a table or excluding a table to a publication additionally requires
+   owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
@@ -200,6 +200,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Adding tables <structname>users</structname> and
+   <structname>departments</structname> that must be excluded from the
+   publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD EXCEPT TABLE users, departments production;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 23d883c158..a0379fb285 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -155,6 +155,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -338,6 +356,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table;
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..4d123d1e5d 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        except tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 2631558ff1..e55ae4211a 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = GetRelationPublications(ancestor, false);
 		List	   *aschemaPubids = NIL;
+		List	   *aexceptpubids;
 
 		level++;
 
@@ -317,7 +319,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		else
 		{
 			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			aexceptpubids = GetRelationPublications(ancestor, true);
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(aexceptpubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -396,6 +400,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
+
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -666,7 +673,7 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 
 /* Gets list of publication oids for a relation */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bexcept)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +687,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (bexcept == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,7 +787,7 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
@@ -787,6 +795,13 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	HeapTuple	tuple;
 	List	   *result = NIL;
 
+	/*
+	 * pg_publication_rel and pg_publication_namespace  will only have except
+	 * tables in case of all tables publication, no need to pass except flag
+	 * to get the relations.
+	 */
+	List	   *exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -802,7 +817,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptpubtablelist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +839,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptpubtablelist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1107,7 +1124,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6df0e6670f..257e669e43 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -297,7 +297,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -324,7 +324,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -373,7 +374,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -392,7 +393,7 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -835,54 +836,53 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								&schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+												PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-								   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	/* tables added through a schema */
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1355,6 +1355,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *tables, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+	bool		nonexcepttable = false;
+	bool		excepttable = false;
+
+	foreach(lc, tables)
+	{
+		PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+		if (!pub_table->except)
+			nonexcepttable = true;
+		else
+			excepttable = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
@@ -1374,12 +1387,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
 	/* Check that user is allowed to manipulate the publication tables. */
-	if (tables && pubform->puballtables)
+	if (nonexcepttable && tables && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
+
+	if (excepttable && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				 errdetail("except table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
 }
 
 /*
@@ -1656,6 +1676,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1728,6 +1749,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2cd8546d47..4d660de55e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16274,7 +16274,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16411,7 +16411,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4..25639f6b02 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -219,6 +219,11 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
+static void preprocess_alltables_pubobj_list(List *pubobjspec_list,
+											 int location,
+											 core_yyscan_t yyscanner);
+static void check_except_in_pubobj_list(List *pubobjspec_list,
+											 core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %}
@@ -455,7 +460,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -493,7 +498,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>	opt_interval interval_second
 %type <str>		unicode_normal_form
 
-%type <boolean> opt_instead
+%type <boolean> opt_instead opt_except
 %type <boolean> opt_unique opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_analyze opt_default opt_recheck
 %type <defelt>	opt_binary copy_delimiter
@@ -9879,12 +9884,17 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *)n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
+					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					preprocess_alltables_pubobj_list(n->pubobjects,
+													 @6,
+													 yyscanner);
 					$$ = (Node *)n;
 				}
 			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
@@ -9894,6 +9904,7 @@ CreatePublicationStmt:
 					n->options = $6;
 					n->pubobjects = (List *)$5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_except_in_pubobj_list(n->pubobjects, yyscanner);
 					$$ = (Node *)n;
 				}
 		;
@@ -9912,26 +9923,30 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			opt_except TABLE relation_expr opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
-					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->except = $1;
+					$$->pubtable->relation = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
 					$$->name = $5;
+					$$->except = false;
 					$$->location = @5;
 				}
 			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->except = false;
 					$$->location = @5;
 				}
 			| ColId opt_column_list OptWhereClause
@@ -9995,6 +10010,17 @@ pub_obj_list: 	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ except_pub_obj_list:	pub_obj_list
+					{ $$ = $1; }
+			| /*EMPTY*/
+					{ $$ = NULL; }
+	;
+
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -18712,6 +18738,7 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	ListCell   *cell;
 	PublicationObjSpec *pubobj;
 	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_CONTINUATION;
+	bool prevexceptobj = false;
 
 	if (!pubobjspec_list)
 		return;
@@ -18729,7 +18756,10 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_CONTINUATION)
+		{
 			pubobj->pubobjtype = prevobjtype;
+			pubobj->except = prevexceptobj;
+		}
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
 		{
@@ -18750,6 +18780,8 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 				pubobj->pubtable = pubtable;
 				pubobj->name = NULL;
 			}
+
+			pubobj->pubtable->except = pubobj->except;
 		}
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA)
@@ -18784,6 +18816,60 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		}
 
 		prevobjtype = pubobj->pubobjtype;
+		prevexceptobj = pubobj->except;
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if any other option other that
+ * "EXCEPT TABLE" is specified with "ALL TABLES" and throw an
+ * error.
+ */
+static void
+preprocess_alltables_pubobj_list(List *pubobjspec_list, int location,
+								 core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		PublicationObjSpec *pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* Only EXCEPT TABLE option supported with ALL TABLES */
+		if (!pubobj->except)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("only EXCEPT TABLE can be specified with ALL TABLES option"),
+					parser_errposition(pubobj->location));
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if "EXCEPT TABLES" is specified
+ * with "ALL TABLES" and throw an error.
+ */
+static void
+check_except_in_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* EXCEPT TABLE option supported only with ALL TABLES */
+		if (pubobj->except)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("EXCEPT TABLE can be specified only with ALL TABLES option"),
+					parser_errposition(pubobj->location));
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b197bfd565..4dcd35d1f5 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1988,7 +1988,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2077,22 +2078,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid	pub_relid = relid;
 			int	ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition root
-			 * and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2111,7 +2096,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2126,6 +2112,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2201,6 +2189,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43f14c233d..56592afac1 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5562,6 +5562,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5595,7 +5597,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5609,14 +5611,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5645,7 +5652,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5662,7 +5669,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d3588607e7..d22a81e790 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -285,7 +285,8 @@ static void dumpBlob(Archive *fout, const BlobInfo *binfo);
 static int	dumpBlobs(Archive *fout, const void *arg);
 static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
-static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
+static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+								 bool bexcept);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
@@ -4150,6 +4151,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,7 +4164,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
+							 "SELECT tableoid, oid, prpubid, prrelid, prexcept,"
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4188,6 +4190,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4199,6 +4202,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char       *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4219,7 +4223,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4313,13 +4321,15 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
  *	  dump the definition of the given publication table mapping
  */
 static void
-dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
+dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+					 bool bexcept)
 {
 	DumpOptions *dopt = fout->dopt;
 	PublicationInfo *pubinfo = pubrinfo->publication;
 	TableInfo  *tbinfo = pubrinfo->pubtable;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4329,8 +4339,15 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
 					  fmtId(pubinfo->dobj.name));
+
+	if (bexcept)
+		appendPQExpBufferStr(query, "EXCEPT ");
+
+	appendPQExpBufferStr(query, "TABLE ONLY");
+	description = (bexcept) ? "PUBLICATION EXCEPT TABLE" : "PUBLICATION TABLE";
+
 	appendPQExpBuffer(query, " %s",
 					  fmtQualifiedDumpable(tbinfo));
 
@@ -4359,7 +4376,7 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLE",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9935,7 +9952,10 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
 		case DO_PUBLICATION_REL:
-			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, false);
+			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, true);
 			break;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
@@ -17868,6 +17888,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
 				/* Post-data objects: must come after the post-data boundary */
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a116f4da97 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1488,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION TABLE (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLES IN SCHEMA (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1ecfd7ae23..1837c1878d 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2558,6 +2567,20 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'ALTER PUBLICATION pub5 ADD EXCEPT TABLE test_table' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD EXCEPT TABLE ONLY dump_test.test_table;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4369f2235b..89db708713 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2952,15 +2952,20 @@ describeOneTableDetails(const char *schemaname,
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "WHERE pr.prrelid = '%s' AND pr.prexcept = 'f'\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "		AND NOT EXISTS (SELECT 1\n"
+								  "							FROM pg_catalog.pg_publication_rel pr\n"
+								  "								JOIN pg_catalog.pg_class pc\n"
+								  "	  	 						ON pr.prrelid = pc.oid\n"
+								  "							WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
 								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  oid, oid, oid, oid, oid);
 			}
 			else
 			{
@@ -6303,6 +6308,7 @@ describePublications(const char *pattern)
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n"
+							  "  AND pr.prexcept = 'f'\n"
 							  "ORDER BY 1,2", pubid);
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
@@ -6322,6 +6328,21 @@ describePublications(const char *pattern)
 			}
 		}
 
+		if (pset.sversion >= 150000)
+		{
+			/* Get the except tables for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+							  "FROM pg_catalog.pg_class c\n"
+							  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+							  "WHERE pr.prpubid = '%s'\n"
+							  "  AND pr.prexcept = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Except tables:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 588c0841fe..7870e16acd 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,8 +1822,11 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "EXCEPT"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "EXCEPT", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
@@ -1845,10 +1848,14 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
@@ -2985,7 +2992,9 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 29b1856665..eeee96f42d 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool bexcept);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 4feb581899..2eb1fbeabd 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* except the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index ae87caf089..a515cdb802 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,8 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot, bool alltables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+												List *ancestors, bool pubviaroot, bool alltables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index da02658c81..308d3c07a8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4003,6 +4003,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* except relation */
 } PublicationTable;
 
 /*
@@ -4023,6 +4024,7 @@ typedef struct PublicationObjSpec
 	PublicationObjSpecType pubobjtype;	/* type of this publication object */
 	char	   *name;
 	PublicationTable *pubtable;
+	bool		except;
 	int			location;		/* token location, or -1 if unknown */
 } PublicationObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 398c0f38f6..5d213309e4 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -116,6 +116,35 @@ ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 Tables from schemas:
     "pub_test"
 
+-- should be able to add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD EXCEPT TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- should be able to set except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Except tables:
+    "public.testpub_tbl2"
+
+-- should be able to drop except table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+(1 row)
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -141,6 +170,30 @@ ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 Tables:
     "pub_test.testpub_nopk"
 
+-- fail - can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop except table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't add except table to schema publication
+ALTER PUBLICATION testpub_forschema ADD EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop except table from schema publication
+ALTER PUBLICATION testpub_forschema DROP EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set except table to schema  publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -165,8 +218,34 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- fail - can't specify except table along with table publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...able_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TAB...
+                                                             ^
+-- fail - can't specify except table along with schema publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...le_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TAB...
+                                                             ^
+-- fail - can't specify only except table while create publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...EATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TAB...
+                                                             ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..a6e6543d71 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -71,6 +71,16 @@ ALTER PUBLICATION testpub_fortable DROP ALL TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 \dRp+ testpub_fortable
 
+-- should be able to add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD EXCEPT TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+-- should be able to set except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+-- should be able to drop except table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -85,12 +95,40 @@ ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk;
 ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
 
+-- fail - can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD EXCEPT TABLE testpub_tbl1;
+-- fail - can't drop except table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP EXCEPT TABLE testpub_tbl1;
+-- fail - can't set except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't add except table to schema publication
+ALTER PUBLICATION testpub_forschema ADD EXCEPT TABLE testpub_tbl1;
+-- fail - can't drop except table from schema publication
+ALTER PUBLICATION testpub_forschema DROP EXCEPT TABLE testpub_tbl1;
+-- fail - can't set except table to schema  publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+
+-- fail - can't specify except table along with table publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't specify except table along with schema publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't specify only except table while create publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TABLE testpub_tbl1;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/033_rep_changes_except_table.pl b/src/test/subscription/t/033_rep_changes_except_table.pl
new file mode 100644
index 0000000000..1f6141ceca
--- /dev/null
+++ b/src/test/subscription/t/033_rep_changes_except_table.pl
@@ -0,0 +1,97 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is excluded for excluded tables');
+
+# Insert some data into few tables and verify that inserted data is not
+# replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add EXCEPT TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop except table public.tab1 and verify that subscriber
+# gets the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop EXCEPT TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#17Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: vignesh C (#1)
Re: Skipping schema changes in publication

On Tue, Mar 22, 2022 at 12:39 PM vignesh C <vignesh21@gmail.com> wrote:

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.
This feature is for the pg16 version.
Thoughts?

The feature seems to be useful especially when there are lots of
schemas in a database. However, I don't quite like the syntax. Do we
have 'SKIP' identifier in any of the SQL statements in SQL standard?
Can we think of adding skip_schema_list as an option, something like
below?

CREATE PUBLICATION foo FOR ALL TABLES (skip_schema_list = 's1, s2');
ALTER PUBLICATION foo SET (skip_schema_list = 's1, s2'); - to set
ALTER PUBLICATION foo SET (skip_schema_list = ''); - to reset

Regards,
Bharath Rupireddy.

#18Peter Smith
smithpb2250@gmail.com
In reply to: Bharath Rupireddy (#17)
Re: Skipping schema changes in publication

On Sat, Apr 23, 2022 at 2:09 AM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:

On Tue, Mar 22, 2022 at 12:39 PM vignesh C <vignesh21@gmail.com> wrote:

Hi,

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

A new column pnskip is added to table "pg_publication_namespace", to
maintain the schemas that the user wants to skip publishing through
the publication. Modified the output plugin (pgoutput) to skip
publishing the changes if the relation is part of skip schema
publication.
As a continuation to this, I will work on implementing skipping tables
from all tables in schema and skipping tables from all tables
publication.

Attached patch has the implementation for this.
This feature is for the pg16 version.
Thoughts?

The feature seems to be useful especially when there are lots of
schemas in a database. However, I don't quite like the syntax. Do we
have 'SKIP' identifier in any of the SQL statements in SQL standard?
Can we think of adding skip_schema_list as an option, something like
below?

CREATE PUBLICATION foo FOR ALL TABLES (skip_schema_list = 's1, s2');
ALTER PUBLICATION foo SET (skip_schema_list = 's1, s2'); - to set
ALTER PUBLICATION foo SET (skip_schema_list = ''); - to reset

I had been wondering for some time if there was any way to introduce a
more flexible pattern matching into PUBLICATION but without bloating
the syntax. Maybe your idea to use an option for the "skip" gives a
way to do it...

For example, if we could use regex (for <schemaname>.<tablename>
patterns) for the option value then....

~~

e.g.1. Exclude certain tables:

// do NOT publish any tables of schemas s1,s2
CREATE PUBLICATION foo FOR ALL TABLES (exclude_match = '(s1\..*)|(s2\..*)');

// do NOT publish my secret tables (those called "mysecretXXX")
CREATE PUBLICATION foo FOR ALL TABLES (exclude_match = '(.*\.mysecret.*)');

~~

e.g.2. Only allow certain tables.

// ONLY publish my tables (those called "mytableXXX")
CREATE PUBLICATION foo FOR ALL TABLES (subset_match = '(.*\.mytable.*)');

// So following is equivalent to FOR ALL TABLES IN SCHEMA s1
CREATE PUBLICATION foo FOR ALL TABLES (subset_match = '(s1\..*)');

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

#19osumi.takamichi@fujitsu.com
osumi.takamichi@fujitsu.com
In reply to: vignesh C (#16)
RE: Skipping schema changes in publication

On Thursday, April 21, 2022 12:15 PM vignesh C <vignesh21@gmail.com> wrote:

Updated patch by changing the syntax to use EXCEPT instead of SKIP.

Hi

This is my review comments on the v2 patch.

(1) gram.y

I think we can make a unified function that merges
preprocess_alltables_pubobj_list with check_except_in_pubobj_list.

With regard to preprocess_alltables_pubobj_list,
we don't use the 2nd argument "location" in this function.

(2) create_publication.sgml

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table;

This sentence should end ":" not ";".

(3) publication.out & publication.sql

+-- fail - can't set except table to schema  publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;

There is one unnecessary space in the comment.
Kindly change from "schema publication" to "schema publication".

(4) pg_dump.c & describe.c

In your first email of this thread, you explained this feature
is for PG16. Don't we need additional branch for PG16 ?

@@ -6322,6 +6328,21 @@ describePublications(const char *pattern)
}
}

+               if (pset.sversion >= 150000)
+               {
@@ -4162,7 +4164,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
        /* Collect all publication membership info. */
        if (fout->remoteVersion >= 150000)
                appendPQExpBufferStr(query,
-                                                        "SELECT tableoid, oid, prpubid, prrelid, "
+                                                        "SELECT tableoid, oid, prpubid, prrelid, prexcept,"

(5) psql-ref.sgml

+        If <literal>+</literal> is appended to the command name, the tables,
+        except tables and schemas associated with each publication are shown as
+        well.

I'm not sure if "except tables" is a good description.
I suggest "excluded tables". This applies to the entire patch,
in case if this is reasonable suggestion.

Best Regards,
Takamichi Osumi

#20vignesh C
vignesh21@gmail.com
In reply to: osumi.takamichi@fujitsu.com (#19)
1 attachment(s)
Re: Skipping schema changes in publication

On Tue, Apr 26, 2022 at 11:32 AM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Thursday, April 21, 2022 12:15 PM vignesh C <vignesh21@gmail.com> wrote:

Updated patch by changing the syntax to use EXCEPT instead of SKIP.

Hi

This is my review comments on the v2 patch.

(1) gram.y

I think we can make a unified function that merges
preprocess_alltables_pubobj_list with check_except_in_pubobj_list.

With regard to preprocess_alltables_pubobj_list,
we don't use the 2nd argument "location" in this function.

Removed location and made a unified function.

(2) create_publication.sgml

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table;

This sentence should end ":" not ";".

Modified

(3) publication.out & publication.sql

+-- fail - can't set except table to schema  publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;

There is one unnecessary space in the comment.
Kindly change from "schema publication" to "schema publication".

Modified

(4) pg_dump.c & describe.c

In your first email of this thread, you explained this feature
is for PG16. Don't we need additional branch for PG16 ?

@@ -6322,6 +6328,21 @@ describePublications(const char *pattern)
}
}

+               if (pset.sversion >= 150000)
+               {
@@ -4162,7 +4164,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
/* Collect all publication membership info. */
if (fout->remoteVersion >= 150000)
appendPQExpBufferStr(query,
-                                                        "SELECT tableoid, oid, prpubid, prrelid, "
+                                                        "SELECT tableoid, oid, prpubid, prrelid, prexcept,"

Modified by adding a comment saying "FIXME: 150000 should be changed
to 160000 later for PG16."

(5) psql-ref.sgml

+        If <literal>+</literal> is appended to the command name, the tables,
+        except tables and schemas associated with each publication are shown as
+        well.

I'm not sure if "except tables" is a good description.
I suggest "excluded tables". This applies to the entire patch,
in case if this is reasonable suggestion.

Modified it in most of the places where it was applicable. I felt the
usage was ok in a few places.

Thanks for the comments, the attached v3 patch has the changes for the same.

Regards.
Vignesh

Attachments:

v3-0001-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From f8a8dff478638f822377f2515f69df9c6cce501e Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Wed, 20 Apr 2022 11:19:50 +0530
Subject: [PATCH v3] Skip publishing the tables specified in EXCEPT TABLE.

A new option "EXCEPT TABLE" in Create/Alter Publication allows
one or more tables to be excluded, publisher will exclude sending the data
of the excluded tables to the subscriber.

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD EXCEPT TABLE t1,t2;

A new column prexcept is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude publishing through the publication.
Modified the output plugin (pgoutput) to exclude publishing the changes of the
excluded tables.

Updates pg_dump to identify and dump the excluded tables of the publications.
Updates the \d family of commands to display excluded tables of the
publications and \dRp+ variant will now display associated except tables if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 ++
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  14 ++-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  36 ++++--
 src/backend/commands/publicationcmds.c        | 106 +++++++++++-------
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  78 +++++++++++--
 src/backend/replication/pgoutput/pgoutput.c   |  25 ++---
 src/backend/utils/cache/relcache.c            |  17 ++-
 src/bin/pg_dump/pg_dump.c                     |  45 ++++++--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  23 ++++
 src/bin/psql/describe.c                       |  52 +++++++--
 src/bin/psql/tab-complete.c                   |  15 ++-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   4 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  81 ++++++++++++-
 src/test/regress/sql/publication.sql          |  40 ++++++-
 .../t/033_rep_changes_except_table.pl         |  97 ++++++++++++++++
 24 files changed, 588 insertions(+), 118 deletions(-)
 create mode 100644 src/test/subscription/t/033_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a533a2153e..78e8c22a59 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the table must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..fbed735066 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1165,10 +1165,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To add tables or exclude tables to a publication, the user must have
+   ownership rights on the table. To add all tables in schema to a publication,
+   the user must be a superuser. To create a publication that publishes all
+   tables or all tables in schema automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..2a8e4e041b 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
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    [EXCEPT] TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -70,8 +70,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
+   Adding a table or excluding a table to a publication additionally requires
+   owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
@@ -200,6 +200,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Adding tables <structname>users</structname> and
+   <structname>departments</structname> that must be excluded from the
+   publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD EXCEPT TABLE users, departments production;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..f934472db2 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -156,6 +156,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +369,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..3889796b3f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 2631558ff1..6a5a910a30 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = GetRelationPublications(ancestor, false);
 		List	   *aschemaPubids = NIL;
+		List	   *aexceptpubids;
 
 		level++;
 
@@ -317,7 +319,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		else
 		{
 			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			aexceptpubids = GetRelationPublications(ancestor, true);
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(aexceptpubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -396,6 +400,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
+
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -666,7 +673,7 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 
 /* Gets list of publication oids for a relation */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bexcept)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +687,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (bexcept == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,7 +787,7 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
@@ -787,6 +795,13 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	HeapTuple	tuple;
 	List	   *result = NIL;
 
+	/*
+	 * pg_publication_rel and pg_publication_namespace  will only have excluded
+	 * tables in case of all tables publication, no need to pass except flag
+	 * to get the relations.
+	 */
+	List	   *exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -802,7 +817,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptpubtablelist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +839,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptpubtablelist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1107,7 +1124,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6df0e6670f..257e669e43 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -297,7 +297,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -324,7 +324,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -373,7 +374,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -392,7 +393,7 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -835,54 +836,53 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								&schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+												PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-								   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	/* tables added through a schema */
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1355,6 +1355,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *tables, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+	bool		nonexcepttable = false;
+	bool		excepttable = false;
+
+	foreach(lc, tables)
+	{
+		PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+		if (!pub_table->except)
+			nonexcepttable = true;
+		else
+			excepttable = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
@@ -1374,12 +1387,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
 	/* Check that user is allowed to manipulate the publication tables. */
-	if (tables && pubform->puballtables)
+	if (nonexcepttable && tables && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
+
+	if (excepttable && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				 errdetail("except table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
 }
 
 /*
@@ -1656,6 +1676,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1728,6 +1749,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2cd8546d47..4d660de55e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16274,7 +16274,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16411,7 +16411,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4..4a2d83dacd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -219,6 +219,8 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
+static void check_except_pubobjs(List *pubobjspec_list, core_yyscan_t yyscanner,
+								 bool alltables);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %}
@@ -455,7 +457,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -493,7 +495,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>	opt_interval interval_second
 %type <str>		unicode_normal_form
 
-%type <boolean> opt_instead
+%type <boolean> opt_instead opt_except
 %type <boolean> opt_unique opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_analyze opt_default opt_recheck
 %type <defelt>	opt_binary copy_delimiter
@@ -9879,12 +9881,15 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *)n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
+					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_except_pubobjs(n->pubobjects, yyscanner, true);
 					$$ = (Node *)n;
 				}
 			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
@@ -9894,6 +9899,7 @@ CreatePublicationStmt:
 					n->options = $6;
 					n->pubobjects = (List *)$5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_except_pubobjs(n->pubobjects, yyscanner, false);
 					$$ = (Node *)n;
 				}
 		;
@@ -9912,26 +9918,30 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			opt_except TABLE relation_expr opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
-					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->except = $1;
+					$$->pubtable->relation = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
 					$$->name = $5;
+					$$->except = false;
 					$$->location = @5;
 				}
 			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->except = false;
 					$$->location = @5;
 				}
 			| ColId opt_column_list OptWhereClause
@@ -9995,6 +10005,17 @@ pub_obj_list: 	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ except_pub_obj_list:	pub_obj_list
+					{ $$ = $1; }
+			| /*EMPTY*/
+					{ $$ = NULL; }
+	;
+
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -18712,6 +18733,7 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	ListCell   *cell;
 	PublicationObjSpec *pubobj;
 	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_CONTINUATION;
+	bool prevexceptobj = false;
 
 	if (!pubobjspec_list)
 		return;
@@ -18729,7 +18751,10 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_CONTINUATION)
+		{
 			pubobj->pubobjtype = prevobjtype;
+			pubobj->except = prevexceptobj;
+		}
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
 		{
@@ -18750,6 +18775,8 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 				pubobj->pubtable = pubtable;
 				pubobj->name = NULL;
 			}
+
+			pubobj->pubtable->except = pubobj->except;
 		}
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA)
@@ -18784,6 +18811,41 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		}
 
 		prevobjtype = pubobj->pubobjtype;
+		prevexceptobj = pubobj->except;
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if "EXCEPT TABLES" is specified only
+ * with "ALL TABLES" and "EXCEPT TABLES" are not specified with "NON ALL TABLES"
+ * publications.
+ */
+static void
+check_except_pubobjs(List *pubobjspec_list, core_yyscan_t yyscanner,
+							bool alltables)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* EXCEPT TABLE option supported only with ALL TABLES */
+		if (!alltables && pubobj->except)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("EXCEPT TABLE can be specified only with ALL TABLES option"),
+					parser_errposition(pubobj->location));
+		/* Only EXCEPT TABLE option supported with ALL TABLES */
+		else if (alltables && !pubobj->except)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("only EXCEPT TABLE can be specified with ALL TABLES option"),
+					parser_errposition(pubobj->location));
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b197bfd565..4dcd35d1f5 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1988,7 +1988,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2077,22 +2078,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid	pub_relid = relid;
 			int	ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition root
-			 * and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2111,7 +2096,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2126,6 +2112,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2201,6 +2189,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43f14c233d..56592afac1 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5562,6 +5562,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5595,7 +5597,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5609,14 +5611,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5645,7 +5652,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5662,7 +5669,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 786d592e2b..9371e3ba44 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -285,7 +285,8 @@ static void dumpBlob(Archive *fout, const BlobInfo *binfo);
 static int	dumpBlobs(Archive *fout, const void *arg);
 static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
-static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
+static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+								 bool bexcept);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
@@ -4151,6 +4152,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4164,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							"SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4185,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4201,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4213,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char       *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4234,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4314,13 +4332,15 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
  *	  dump the definition of the given publication table mapping
  */
 static void
-dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
+dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+					 bool bexcept)
 {
 	DumpOptions *dopt = fout->dopt;
 	PublicationInfo *pubinfo = pubrinfo->publication;
 	TableInfo  *tbinfo = pubrinfo->pubtable;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4330,8 +4350,15 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
 					  fmtId(pubinfo->dobj.name));
+
+	if (bexcept)
+		appendPQExpBufferStr(query, "EXCEPT ");
+
+	appendPQExpBufferStr(query, "TABLE ONLY");
+	description = (bexcept) ? "PUBLICATION EXCEPT TABLE" : "PUBLICATION TABLE";
+
 	appendPQExpBuffer(query, " %s",
 					  fmtQualifiedDumpable(tbinfo));
 
@@ -4360,7 +4387,7 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLE",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9936,7 +9963,10 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
 		case DO_PUBLICATION_REL:
-			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, false);
+			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, true);
 			break;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
@@ -17869,6 +17899,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
 				/* Post-data objects: must come after the post-data boundary */
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a116f4da97 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1488,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION TABLE (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLES IN SCHEMA (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 3b31e13f62..9b037d1eb4 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2558,6 +2567,20 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'ALTER PUBLICATION pub5 ADD EXCEPT TABLE test_table' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD EXCEPT TABLE ONLY dump_test.test_table;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4369f2235b..1057e9e7cf 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2952,15 +2952,32 @@ describeOneTableDetails(const char *schemaname,
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
-								  "UNION\n"
+								  "WHERE pr.prrelid = '%s'",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+				appendPQExpBuffer(&buf, "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "		AND NOT EXISTS (SELECT 1\n"
+									  "							FROM pg_catalog.pg_publication_rel pr\n"
+									  "								JOIN pg_catalog.pg_class pc\n"
+									  "	  	 						ON pr.prrelid = pc.oid\n"
+									  "							WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6319,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND pr.prexcept = 'f'\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6322,6 +6344,22 @@ describePublications(const char *pattern)
 			}
 		}
 
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (pset.sversion >= 150000)
+		{
+			/* Get the excluded tables for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+							  "FROM pg_catalog.pg_class c\n"
+							  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+							  "WHERE pr.prpubid = '%s'\n"
+							  "  AND pr.prexcept = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Except tables:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 588c0841fe..7870e16acd 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,8 +1822,11 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "EXCEPT"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "EXCEPT", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
@@ -1845,10 +1848,14 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
@@ -2985,7 +2992,9 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 29b1856665..eeee96f42d 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool bexcept);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 4feb581899..2eb1fbeabd 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* except the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index ae87caf089..a515cdb802 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,8 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot, bool alltables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+												List *ancestors, bool pubviaroot, bool alltables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index da02658c81..308d3c07a8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4003,6 +4003,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* except relation */
 } PublicationTable;
 
 /*
@@ -4023,6 +4024,7 @@ typedef struct PublicationObjSpec
 	PublicationObjSpecType pubobjtype;	/* type of this publication object */
 	char	   *name;
 	PublicationTable *pubtable;
+	bool		except;
 	int			location;		/* token location, or -1 if unknown */
 } PublicationObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 398c0f38f6..630830a8ce 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -116,6 +116,35 @@ ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 Tables from schemas:
     "pub_test"
 
+-- should be able to add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD EXCEPT TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- should be able to set except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Except tables:
+    "public.testpub_tbl2"
+
+-- should be able to drop except table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+(1 row)
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -141,6 +170,30 @@ ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 Tables:
     "pub_test.testpub_nopk"
 
+-- fail - can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop except table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't add except table to schema publication
+ALTER PUBLICATION testpub_forschema ADD EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop except table from schema publication
+ALTER PUBLICATION testpub_forschema DROP EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set except table to schema publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -165,8 +218,34 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- fail - can't specify except table along with table publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...able_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TAB...
+                                                             ^
+-- fail - can't specify except table along with schema publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...le_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TAB...
+                                                             ^
+-- fail - can't specify only except table while create publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...EATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TAB...
+                                                             ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..746098ecb4 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -71,6 +71,16 @@ ALTER PUBLICATION testpub_fortable DROP ALL TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 \dRp+ testpub_fortable
 
+-- should be able to add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD EXCEPT TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+-- should be able to set except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+-- should be able to drop except table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -85,12 +95,40 @@ ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk;
 ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
 
+-- fail - can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD EXCEPT TABLE testpub_tbl1;
+-- fail - can't drop except table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP EXCEPT TABLE testpub_tbl1;
+-- fail - can't set except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't add except table to schema publication
+ALTER PUBLICATION testpub_forschema ADD EXCEPT TABLE testpub_tbl1;
+-- fail - can't drop except table from schema publication
+ALTER PUBLICATION testpub_forschema DROP EXCEPT TABLE testpub_tbl1;
+-- fail - can't set except table to schema publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+
+-- fail - can't specify except table along with table publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't specify except table along with schema publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't specify only except table while create publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TABLE testpub_tbl1;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/033_rep_changes_except_table.pl b/src/test/subscription/t/033_rep_changes_except_table.pl
new file mode 100644
index 0000000000..1f6141ceca
--- /dev/null
+++ b/src/test/subscription/t/033_rep_changes_except_table.pl
@@ -0,0 +1,97 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is excluded for excluded tables');
+
+# Insert some data into few tables and verify that inserted data is not
+# replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add EXCEPT TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop except table public.tab1 and verify that subscriber
+# gets the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop EXCEPT TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#21osumi.takamichi@fujitsu.com
osumi.takamichi@fujitsu.com
In reply to: vignesh C (#20)
RE: Skipping schema changes in publication

On Wednesday, April 27, 2022 9:50 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v3 patch has the changes for the same.

Hi

Thank you for updating the patch. Several minor comments on v3.

(1) commit message

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD EXCEPT TABLE t1,t2;

We have above sentence, but it looks better
to make the description a bit more accurate.

Kindly change
From :
"The new syntax allows specifying schemas"
To :
"The new syntax allows specifying excluded relations"

Also, kindly change "OR" to "or",
because this description is not syntax.

(2) publication_add_relation

@@ -396,6 +400,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
                ObjectIdGetDatum(pubid);
        values[Anum_pg_publication_rel_prrelid - 1] =
                ObjectIdGetDatum(relid);
+       values[Anum_pg_publication_rel_prexcept - 1] =
+               BoolGetDatum(pri->except);
+

/* Add qualifications, if available */

It would be better to remove the blank line,
because with this change, we'll have two blank
lines in a row.

(3) pg_dump.h & pg_dump_sort.c

@@ -80,6 +80,7 @@ typedef enum
DO_REFRESH_MATVIEW,
DO_POLICY,
DO_PUBLICATION,
+ DO_PUBLICATION_EXCEPT_REL,
DO_PUBLICATION_REL,
DO_PUBLICATION_TABLE_IN_SCHEMA,
DO_SUBSCRIPTION

@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
        PRIO_FK_CONSTRAINT,
        PRIO_POLICY,
        PRIO_PUBLICATION,
+       PRIO_PUBLICATION_EXCEPT_REL,
        PRIO_PUBLICATION_REL,
        PRIO_PUBLICATION_TABLE_IN_SCHEMA,
        PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
        PRIO_REFRESH_MATVIEW,           /* DO_REFRESH_MATVIEW */
        PRIO_POLICY,                            /* DO_POLICY */
        PRIO_PUBLICATION,                       /* DO_PUBLICATION */
+       PRIO_PUBLICATION_EXCEPT_REL,    /* DO_PUBLICATION_EXCEPT_REL */
        PRIO_PUBLICATION_REL,           /* DO_PUBLICATION_REL */
        PRIO_PUBLICATION_TABLE_IN_SCHEMA,       /* DO_PUBLICATION_TABLE_IN_SCHEMA */
        PRIO_SUBSCRIPTION                       /* DO_SUBSCRIPTION */

How about having similar order between
pg_dump.h and pg_dump_sort.c, like
we'll add DO_PUBLICATION_EXCEPT_REL
after DO_PUBLICATION_REL in pg_dump.h ?

(4) GetAllTablesPublicationRelations

+       /*
+        * pg_publication_rel and pg_publication_namespace  will only have except
+        * tables in case of all tables publication, no need to pass except flag
+        * to get the relations.
+        */
+       List       *exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+

There is one unnecessary space in a comment
"...pg_publication_namespace will only have...". Kindly remove it.

Then, how about diving the variable declaration and
the insertion of the return value of GetPublicationRelations ?
That might be aligned with other places in this file.

(5) GetTopMostAncestorInPublication

@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
        foreach(lc, ancestors)
        {
                Oid                     ancestor = lfirst_oid(lc);
-               List       *apubids = GetRelationPublications(ancestor);
+               List       *apubids = GetRelationPublications(ancestor, false);
                List       *aschemaPubids = NIL;
+               List       *aexceptpubids;

level++;

@@ -317,7 +319,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
                else
                {
                        aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-                       if (list_member_oid(aschemaPubids, puboid))
+                       aexceptpubids = GetRelationPublications(ancestor, true);
+                       if (list_member_oid(aschemaPubids, puboid) ||
+                               (puballtables && !list_member_oid(aexceptpubids, puboid)))
                        {
                                topmost_relid = ancestor;

It seems we forgot to call list_free for "aexceptpubids".

Best Regards,
Takamichi Osumi

#22Amit Kapila
amit.kapila16@gmail.com
In reply to: Bharath Rupireddy (#17)
Re: Skipping schema changes in publication

On Fri, Apr 22, 2022 at 9:39 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:

On Tue, Mar 22, 2022 at 12:39 PM vignesh C <vignesh21@gmail.com> wrote:

This feature adds an option to skip changes of all tables in specified
schema while creating publication.
This feature is helpful for use cases where the user wants to
subscribe to all the changes except for the changes present in a few
schemas.
Ex:
CREATE PUBLICATION pub1 FOR ALL TABLES SKIP ALL TABLES IN SCHEMA s1,s2;
OR
ALTER PUBLICATION pub1 ADD SKIP ALL TABLES IN SCHEMA s1,s2;

The feature seems to be useful especially when there are lots of
schemas in a database. However, I don't quite like the syntax. Do we
have 'SKIP' identifier in any of the SQL statements in SQL standard?

After discussion, it seems EXCEPT is a preferred choice and the same
is used in the other existing syntax as well.

Can we think of adding skip_schema_list as an option, something like
below?

CREATE PUBLICATION foo FOR ALL TABLES (skip_schema_list = 's1, s2');
ALTER PUBLICATION foo SET (skip_schema_list = 's1, s2'); - to set
ALTER PUBLICATION foo SET (skip_schema_list = ''); - to reset

Yeah, that is also an option but it seems it will be difficult to
extend if want to support "all columns except (c1, ..)" for the column
list feature.

The other thing to decide is for which all objects we want to support
EXCEPT clause as it may not be useful for everything as indicated by
Peter E. and Euler. We have seen that Oracle supports "all columns
except (c1, ..)" [1]https://dev.mysql.com/doc/refman/5.7/en/change-replication-filter.html and MySQL seems to support for tables [2]https://docs.oracle.com/en/cloud/paas/goldengate-cloud/gwuad/selecting-columns.html#GUID-9A851C8B-48F7-43DF-8D98-D086BE069E20. I
guess we should restrict ourselves to those two cases for now and then
we can extend it later for schemas if required or people agree. Also,
we should see the syntax we choose here should be extendable.

Another idea that occurred to me today for tables this is as follows:
1. Allow to mention except during create publication ... For All Tables.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
2. Allow to Reset it. This new syntax will reset all objects in the
publications.
Alter Publication ... RESET;
3. Allow to add it to an existing publication
Alter Publication ... Add ALL TABLES [EXCEPT TABLE t1,t2];

I think it can be extended in a similar way for schema syntax as well.

[1]: https://dev.mysql.com/doc/refman/5.7/en/change-replication-filter.html
[2]: https://docs.oracle.com/en/cloud/paas/goldengate-cloud/gwuad/selecting-columns.html#GUID-9A851C8B-48F7-43DF-8D98-D086BE069E20

--
With Regards,
Amit Kapila.

#23vignesh C
vignesh21@gmail.com
In reply to: osumi.takamichi@fujitsu.com (#21)
1 attachment(s)
Re: Skipping schema changes in publication

On Thu, Apr 28, 2022 at 4:50 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Wednesday, April 27, 2022 9:50 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v3 patch has the changes for the same.

Hi

Thank you for updating the patch. Several minor comments on v3.

(1) commit message

The new syntax allows specifying schemas. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
OR
ALTER PUBLICATION pub1 ADD EXCEPT TABLE t1,t2;

We have above sentence, but it looks better
to make the description a bit more accurate.

Kindly change
From :
"The new syntax allows specifying schemas"
To :
"The new syntax allows specifying excluded relations"

Also, kindly change "OR" to "or",
because this description is not syntax.

Slightly reworded and modified

(2) publication_add_relation

@@ -396,6 +400,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
ObjectIdGetDatum(pubid);
values[Anum_pg_publication_rel_prrelid - 1] =
ObjectIdGetDatum(relid);
+       values[Anum_pg_publication_rel_prexcept - 1] =
+               BoolGetDatum(pri->except);
+

/* Add qualifications, if available */

It would be better to remove the blank line,
because with this change, we'll have two blank
lines in a row.

Modified

(3) pg_dump.h & pg_dump_sort.c

@@ -80,6 +80,7 @@ typedef enum
DO_REFRESH_MATVIEW,
DO_POLICY,
DO_PUBLICATION,
+ DO_PUBLICATION_EXCEPT_REL,
DO_PUBLICATION_REL,
DO_PUBLICATION_TABLE_IN_SCHEMA,
DO_SUBSCRIPTION

@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
PRIO_FK_CONSTRAINT,
PRIO_POLICY,
PRIO_PUBLICATION,
+       PRIO_PUBLICATION_EXCEPT_REL,
PRIO_PUBLICATION_REL,
PRIO_PUBLICATION_TABLE_IN_SCHEMA,
PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
PRIO_REFRESH_MATVIEW,           /* DO_REFRESH_MATVIEW */
PRIO_POLICY,                            /* DO_POLICY */
PRIO_PUBLICATION,                       /* DO_PUBLICATION */
+       PRIO_PUBLICATION_EXCEPT_REL,    /* DO_PUBLICATION_EXCEPT_REL */
PRIO_PUBLICATION_REL,           /* DO_PUBLICATION_REL */
PRIO_PUBLICATION_TABLE_IN_SCHEMA,       /* DO_PUBLICATION_TABLE_IN_SCHEMA */
PRIO_SUBSCRIPTION                       /* DO_SUBSCRIPTION */

How about having similar order between
pg_dump.h and pg_dump_sort.c, like
we'll add DO_PUBLICATION_EXCEPT_REL
after DO_PUBLICATION_REL in pg_dump.h ?

Modified

(4) GetAllTablesPublicationRelations

+       /*
+        * pg_publication_rel and pg_publication_namespace  will only have except
+        * tables in case of all tables publication, no need to pass except flag
+        * to get the relations.
+        */
+       List       *exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+

There is one unnecessary space in a comment
"...pg_publication_namespace will only have...". Kindly remove it.

Then, how about diving the variable declaration and
the insertion of the return value of GetPublicationRelations ?
That might be aligned with other places in this file.

Modified

(5) GetTopMostAncestorInPublication

@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
foreach(lc, ancestors)
{
Oid                     ancestor = lfirst_oid(lc);
-               List       *apubids = GetRelationPublications(ancestor);
+               List       *apubids = GetRelationPublications(ancestor, false);
List       *aschemaPubids = NIL;
+               List       *aexceptpubids;

level++;

@@ -317,7 +319,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
else
{
aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-                       if (list_member_oid(aschemaPubids, puboid))
+                       aexceptpubids = GetRelationPublications(ancestor, true);
+                       if (list_member_oid(aschemaPubids, puboid) ||
+                               (puballtables && !list_member_oid(aexceptpubids, puboid)))
{
topmost_relid = ancestor;

It seems we forgot to call list_free for "aexceptpubids".

Modified

The attached v4 patch has the changes for the same.

Regards,
Vignesh

Attachments:

v4-0001-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From b900c9468bc5c9deb3412361c7a7891352b0ffb6 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Wed, 20 Apr 2022 11:19:50 +0530
Subject: [PATCH v4] Skip publishing the tables specified in EXCEPT TABLE.

A new option "EXCEPT TABLE" in Create/Alter Publication allows
one or more tables to be excluded, publisher will exclude sending the data
of the excluded tables to the subscriber.

The new syntax allows specifying exclude relations while creating a publication
or add/drop/set exclude relations in alter publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 DROP EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 SET EXCEPT TABLE t1,t2;

A new column prexcept is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude publishing through the publication.
Modified the output plugin (pgoutput) to exclude publishing the changes of the
excluded tables.

Updates pg_dump to identify and dump the excluded tables of the publications.
Updates the \d family of commands to display excluded tables of the
publications and \dRp+ variant will now display associated except tables if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 ++
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  14 ++-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  38 +++++--
 src/backend/commands/publicationcmds.c        | 106 +++++++++++-------
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  78 +++++++++++--
 src/backend/replication/pgoutput/pgoutput.c   |  25 ++---
 src/backend/utils/cache/relcache.c            |  17 ++-
 src/bin/pg_dump/pg_dump.c                     |  45 ++++++--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  23 ++++
 src/bin/psql/describe.c                       |  52 +++++++--
 src/bin/psql/tab-complete.c                   |  15 ++-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   4 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  81 ++++++++++++-
 src/test/regress/sql/publication.sql          |  40 ++++++-
 .../t/033_rep_changes_except_table.pl         |  97 ++++++++++++++++
 24 files changed, 590 insertions(+), 118 deletions(-)
 create mode 100644 src/test/subscription/t/033_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a533a2153e..78e8c22a59 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the table must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..fbed735066 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1165,10 +1165,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To add tables or exclude tables to a publication, the user must have
+   ownership rights on the table. To add all tables in schema to a publication,
+   the user must be a superuser. To create a publication that publishes all
+   tables or all tables in schema automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..2a8e4e041b 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
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    [EXCEPT] TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 </synopsis>
  </refsynopsisdiv>
@@ -70,8 +70,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
+   Adding a table or excluding a table to a publication additionally requires
+   owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
@@ -200,6 +200,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Adding tables <structname>users</structname> and
+   <structname>departments</structname> that must be excluded from the
+   publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD EXCEPT TABLE users, departments production;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..f934472db2 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -156,6 +156,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +369,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..3889796b3f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 2631558ff1..8dcf4e5484 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = GetRelationPublications(ancestor, false);
 		List	   *aschemaPubids = NIL;
+		List	   *aexceptpubids = NIL;
 
 		level++;
 
@@ -317,7 +319,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		else
 		{
 			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			aexceptpubids = GetRelationPublications(ancestor, true);
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(aexceptpubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -328,6 +332,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 
 		list_free(apubids);
 		list_free(aschemaPubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +401,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -666,7 +673,7 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 
 /* Gets list of publication oids for a relation */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bexcept)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +687,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (bexcept == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,7 +787,7 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
@@ -787,6 +795,15 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	HeapTuple	tuple;
 	List	   *result = NIL;
 
+	/*
+	 * pg_publication_rel and pg_publication_namespace will only have excluded
+	 * tables in case of all tables publication, no need to pass except flag
+	 * to get the relations.
+	 */
+	List	   *exceptpubtablelist;
+
+	exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -802,7 +819,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptpubtablelist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +841,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptpubtablelist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1107,7 +1126,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6df0e6670f..257e669e43 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -297,7 +297,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -324,7 +324,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -373,7 +374,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-						 bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -392,7 +393,7 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -835,54 +836,53 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								&schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+												PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-								   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	/* tables added through a schema */
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1355,6 +1355,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *tables, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+	bool		nonexcepttable = false;
+	bool		excepttable = false;
+
+	foreach(lc, tables)
+	{
+		PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+		if (!pub_table->except)
+			nonexcepttable = true;
+		else
+			excepttable = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
@@ -1374,12 +1387,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
 	/* Check that user is allowed to manipulate the publication tables. */
-	if (tables && pubform->puballtables)
+	if (nonexcepttable && tables && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
+
+	if (excepttable && !pubform->puballtables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				 errdetail("except table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
 }
 
 /*
@@ -1656,6 +1676,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1728,6 +1749,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2cd8546d47..4d660de55e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16274,7 +16274,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16411,7 +16411,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4..4a2d83dacd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -219,6 +219,8 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
+static void check_except_pubobjs(List *pubobjspec_list, core_yyscan_t yyscanner,
+								 bool alltables);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %}
@@ -455,7 +457,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -493,7 +495,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>	opt_interval interval_second
 %type <str>		unicode_normal_form
 
-%type <boolean> opt_instead
+%type <boolean> opt_instead opt_except
 %type <boolean> opt_unique opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_analyze opt_default opt_recheck
 %type <defelt>	opt_binary copy_delimiter
@@ -9879,12 +9881,15 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *)n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
+					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_except_pubobjs(n->pubobjects, yyscanner, true);
 					$$ = (Node *)n;
 				}
 			| CREATE PUBLICATION name FOR pub_obj_list opt_definition
@@ -9894,6 +9899,7 @@ CreatePublicationStmt:
 					n->options = $6;
 					n->pubobjects = (List *)$5;
 					preprocess_pubobj_list(n->pubobjects, yyscanner);
+					check_except_pubobjs(n->pubobjects, yyscanner, false);
 					$$ = (Node *)n;
 				}
 		;
@@ -9912,26 +9918,30 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			opt_except TABLE relation_expr opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
-					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->except = $1;
+					$$->pubtable->relation = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_SCHEMA;
 					$$->name = $5;
+					$$->except = false;
 					$$->location = @5;
 				}
 			| ALL TABLES IN_P SCHEMA CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
+					$$->except = false;
 					$$->location = @5;
 				}
 			| ColId opt_column_list OptWhereClause
@@ -9995,6 +10005,17 @@ pub_obj_list: 	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ except_pub_obj_list:	pub_obj_list
+					{ $$ = $1; }
+			| /*EMPTY*/
+					{ $$ = NULL; }
+	;
+
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -18712,6 +18733,7 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 	ListCell   *cell;
 	PublicationObjSpec *pubobj;
 	PublicationObjSpecType prevobjtype = PUBLICATIONOBJ_CONTINUATION;
+	bool prevexceptobj = false;
 
 	if (!pubobjspec_list)
 		return;
@@ -18729,7 +18751,10 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		pubobj = (PublicationObjSpec *) lfirst(cell);
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_CONTINUATION)
+		{
 			pubobj->pubobjtype = prevobjtype;
+			pubobj->except = prevexceptobj;
+		}
 
 		if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLE)
 		{
@@ -18750,6 +18775,8 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 				pubobj->pubtable = pubtable;
 				pubobj->name = NULL;
 			}
+
+			pubobj->pubtable->except = pubobj->except;
 		}
 		else if (pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_SCHEMA ||
 				 pubobj->pubobjtype == PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA)
@@ -18784,6 +18811,41 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 		}
 
 		prevobjtype = pubobj->pubobjtype;
+		prevexceptobj = pubobj->except;
+	}
+}
+
+/*
+ * Process pubobjspec_list to check if "EXCEPT TABLES" is specified only
+ * with "ALL TABLES" and "EXCEPT TABLES" are not specified with "NON ALL TABLES"
+ * publications.
+ */
+static void
+check_except_pubobjs(List *pubobjspec_list, core_yyscan_t yyscanner,
+							bool alltables)
+{
+	ListCell   *cell;
+	PublicationObjSpec *pubobj;
+
+	if (!pubobjspec_list)
+		return;
+
+	foreach(cell, pubobjspec_list)
+	{
+		pubobj = (PublicationObjSpec *) lfirst(cell);
+
+		/* EXCEPT TABLE option supported only with ALL TABLES */
+		if (!alltables && pubobj->except)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("EXCEPT TABLE can be specified only with ALL TABLES option"),
+					parser_errposition(pubobj->location));
+		/* Only EXCEPT TABLE option supported with ALL TABLES */
+		else if (alltables && !pubobj->except)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("only EXCEPT TABLE can be specified with ALL TABLES option"),
+					parser_errposition(pubobj->location));
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b197bfd565..4dcd35d1f5 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1988,7 +1988,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2077,22 +2078,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid	pub_relid = relid;
 			int	ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition root
-			 * and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2111,7 +2096,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2126,6 +2112,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2201,6 +2189,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43f14c233d..56592afac1 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5562,6 +5562,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5595,7 +5597,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5609,14 +5611,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5645,7 +5652,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5662,7 +5669,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-									 pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 786d592e2b..a0b1011269 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -285,7 +285,8 @@ static void dumpBlob(Archive *fout, const BlobInfo *binfo);
 static int	dumpBlobs(Archive *fout, const void *arg);
 static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
-static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
+static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+								 bool bexcept);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
@@ -4151,6 +4152,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4164,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							"SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4185,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4201,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4213,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char       *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4234,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4314,13 +4332,15 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
  *	  dump the definition of the given publication table mapping
  */
 static void
-dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
+dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo,
+					 bool bexcept)
 {
 	DumpOptions *dopt = fout->dopt;
 	PublicationInfo *pubinfo = pubrinfo->publication;
 	TableInfo  *tbinfo = pubrinfo->pubtable;
 	PQExpBuffer query;
 	char	   *tag;
+	char	   *description;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -4330,8 +4350,15 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
 					  fmtId(pubinfo->dobj.name));
+
+	if (bexcept)
+		appendPQExpBufferStr(query, "EXCEPT ");
+
+	appendPQExpBufferStr(query, "TABLE ONLY");
+	description = (bexcept) ? "PUBLICATION EXCEPT TABLE" : "PUBLICATION TABLE";
+
 	appendPQExpBuffer(query, " %s",
 					  fmtQualifiedDumpable(tbinfo));
 
@@ -4360,7 +4387,7 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					 ARCHIVE_OPTS(.tag = tag,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .owner = pubinfo->rolname,
-								  .description = "PUBLICATION TABLE",
+								  .description = description,
 								  .section = SECTION_POST_DATA,
 								  .createStmt = query->data));
 
@@ -9935,8 +9962,11 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, true);
+			break;
 		case DO_PUBLICATION_REL:
-			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
+			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj, false);
 			break;
 		case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			dumpPublicationNamespace(fout,
@@ -17868,6 +17898,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 3b31e13f62..9b037d1eb4 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,15 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
@@ -2558,6 +2567,20 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'ALTER PUBLICATION pub5 ADD EXCEPT TABLE test_table' => {
+		create_order => 52,
+		create_sql =>
+		  'ALTER PUBLICATION pub5 ADD EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QALTER PUBLICATION pub5 ADD EXCEPT TABLE ONLY dump_test.test_table;\E
+			/xm,
+		like   => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SCHEMA public' => {
 		regexp => qr/^CREATE SCHEMA public;/m,
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4369f2235b..1057e9e7cf 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2952,15 +2952,32 @@ describeOneTableDetails(const char *schemaname,
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
-								  "UNION\n"
+								  "WHERE pr.prrelid = '%s'",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+				appendPQExpBuffer(&buf, "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "		AND NOT EXISTS (SELECT 1\n"
+									  "							FROM pg_catalog.pg_publication_rel pr\n"
+									  "								JOIN pg_catalog.pg_class pc\n"
+									  "	  	 						ON pr.prrelid = pc.oid\n"
+									  "							WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6319,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND pr.prexcept = 'f'\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6322,6 +6344,22 @@ describePublications(const char *pattern)
 			}
 		}
 
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (pset.sversion >= 150000)
+		{
+			/* Get the excluded tables for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+							  "FROM pg_catalog.pg_class c\n"
+							  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+							  "WHERE pr.prpubid = '%s'\n"
+							  "  AND pr.prexcept = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Except tables:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 588c0841fe..7870e16acd 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,8 +1822,11 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "EXCEPT"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "EXCEPT", "TABLE") ||
 			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
@@ -1845,10 +1848,14 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	/* ALTER PUBLICATION <name> SET */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
-		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "EXCEPT TABLE", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
@@ -2985,7 +2992,9 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 29b1856665..eeee96f42d 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool bexcept);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 4feb581899..2eb1fbeabd 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* except the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index ae87caf089..a515cdb802 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,8 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot, bool alltables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-									 List *ancestors, bool pubviaroot);
+												List *ancestors, bool pubviaroot, bool alltables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index da02658c81..308d3c07a8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4003,6 +4003,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* except relation */
 } PublicationTable;
 
 /*
@@ -4023,6 +4024,7 @@ typedef struct PublicationObjSpec
 	PublicationObjSpecType pubobjtype;	/* type of this publication object */
 	char	   *name;
 	PublicationTable *pubtable;
+	bool		except;
 	int			location;		/* token location, or -1 if unknown */
 } PublicationObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 398c0f38f6..630830a8ce 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -116,6 +116,35 @@ ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 Tables from schemas:
     "pub_test"
 
+-- should be able to add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD EXCEPT TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- should be able to set except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+Except tables:
+    "public.testpub_tbl2"
+
+-- should be able to drop except table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+                              Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | f       | f         | f
+(1 row)
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -141,6 +170,30 @@ ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 Tables:
     "pub_test.testpub_nopk"
 
+-- fail - can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop except table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_fortable" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't add except table to schema publication
+ALTER PUBLICATION testpub_forschema ADD EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't drop except table from schema publication
+ALTER PUBLICATION testpub_forschema DROP EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
+-- fail - can't set except table to schema publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;
+ERROR:  publication "testpub_forschema" is not defined as FOR ALL TABLES
+DETAIL:  except table cannot be added to, dropped from, or set on NON ALL TABLES publications.
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
        pubname        | puballtables 
 ----------------------+--------------
@@ -165,8 +218,34 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- fail - can't specify except table along with table publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...able_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TAB...
+                                                             ^
+-- fail - can't specify except table along with schema publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...le_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TAB...
+                                                             ^
+-- fail - can't specify only except table while create publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TABLE testpub_tbl1;
+ERROR:  EXCEPT TABLE can be specified only with ALL TABLES option
+LINE 1: ...EATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TAB...
+                                                             ^
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..746098ecb4 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -71,6 +71,16 @@ ALTER PUBLICATION testpub_fortable DROP ALL TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_fortable SET ALL TABLES IN SCHEMA pub_test;
 \dRp+ testpub_fortable
 
+-- should be able to add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables ADD EXCEPT TABLE testpub_tbl1;
+\dRp+ testpub_foralltables
+-- should be able to set except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables SET EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+-- should be able to drop except table from 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_foralltables DROP EXCEPT TABLE testpub_tbl2;
+\dRp+ testpub_foralltables
+
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forschema FOR ALL TABLES IN SCHEMA pub_test;
 RESET client_min_messages;
@@ -85,12 +95,40 @@ ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk;
 ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
 
+-- fail - can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable ADD EXCEPT TABLE testpub_tbl1;
+-- fail - can't drop except table from 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable DROP EXCEPT TABLE testpub_tbl1;
+-- fail - can't set except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_fortable SET EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't add except table to schema publication
+ALTER PUBLICATION testpub_forschema ADD EXCEPT TABLE testpub_tbl1;
+-- fail - can't drop except table from schema publication
+ALTER PUBLICATION testpub_forschema DROP EXCEPT TABLE testpub_tbl1;
+-- fail - can't set except table to schema publication
+ALTER PUBLICATION testpub_forschema SET EXCEPT TABLE testpub_tbl1;
 SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_foralltables';
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+
+-- fail - can't specify except table along with table publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR TABLE pub_test.testpub_nopk, EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't specify except table along with schema publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR ALL TABLES IN SCHEMA pub_test, EXCEPT TABLE testpub_tbl1;
+
+-- fail - can't specify only except table while create publication
+CREATE PUBLICATION testpub_fortable_excepttable FOR EXCEPT TABLE testpub_tbl1;
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
diff --git a/src/test/subscription/t/033_rep_changes_except_table.pl b/src/test/subscription/t/033_rep_changes_except_table.pl
new file mode 100644
index 0000000000..1f6141ceca
--- /dev/null
+++ b/src/test/subscription/t/033_rep_changes_except_table.pl
@@ -0,0 +1,97 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is excluded for excluded tables');
+
+# Insert some data into few tables and verify that inserted data is not
+# replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema add EXCEPT TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# Alter publication to drop except table public.tab1 and verify that subscriber
+# gets the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema drop EXCEPT TABLE public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+        "SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(10|1|10), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#24Peter Smith
smithpb2250@gmail.com
In reply to: Amit Kapila (#22)
Re: Skipping schema changes in publication

On Thu, Apr 28, 2022 at 9:32 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

...

Another idea that occurred to me today for tables this is as follows:
1. Allow to mention except during create publication ... For All Tables.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
2. Allow to Reset it. This new syntax will reset all objects in the
publications.
Alter Publication ... RESET;
3. Allow to add it to an existing publication
Alter Publication ... Add ALL TABLES [EXCEPT TABLE t1,t2];

I think it can be extended in a similar way for schema syntax as well.

Consider if the user does
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT t3,t4;

What does it mean?
e.g. Is there only one exception list that is modified? Or did the ADD
ALL TABLES override all meaning of the original list?
e.g. Are we now skipping t1,t2,t3,t4, or are we now only skipping t3,t4?

~~~

Here is a similar example, where the ADD TABLE seems confusing to me
when it intersects with a prior EXCEPT
e.g.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT t1,t2; // ok
ALTER PUBLICATION pub1 ADD TABLE t1; ???

What does it mean?
e.g. Does the explicit ADD TABLE override the original exception list?
e.g. Is t1 published now or should that ALTER have caused an error?

~~

It feels like there are too many tricky rules when using EXCEPT with
ALTER PUBLICATION. I guess complexities can be described in the
documentation but IMO it would be better if the ALTER syntax could be
unambiguous in the first place. So perhaps the rules should be more
restrictive (e.g. just disallow ALTER ... ADD any table that overlaps
the existing EXCEPT list ??)

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

#25Amit Kapila
amit.kapila16@gmail.com
In reply to: Peter Smith (#24)
Re: Skipping schema changes in publication

On Tue, May 3, 2022 at 2:24 PM Peter Smith <smithpb2250@gmail.com> wrote:

On Thu, Apr 28, 2022 at 9:32 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

...

Another idea that occurred to me today for tables this is as follows:
1. Allow to mention except during create publication ... For All Tables.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
2. Allow to Reset it. This new syntax will reset all objects in the
publications.
Alter Publication ... RESET;
3. Allow to add it to an existing publication
Alter Publication ... Add ALL TABLES [EXCEPT TABLE t1,t2];

I think it can be extended in a similar way for schema syntax as well.

Consider if the user does
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT t3,t4;

What does it mean?
e.g. Is there only one exception list that is modified? Or did the ADD
ALL TABLES override all meaning of the original list?
e.g. Are we now skipping t1,t2,t3,t4, or are we now only skipping t3,t4?

This won't be allowed. We won't allow changing ALL TABLES publication
unless the user first performs RESET. This is the purpose of providing
the RESET variant.

~~~

Here is a similar example, where the ADD TABLE seems confusing to me
when it intersects with a prior EXCEPT
e.g.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT t1,t2; // ok
ALTER PUBLICATION pub1 ADD TABLE t1; ???

What does it mean?
e.g. Does the explicit ADD TABLE override the original exception list?
e.g. Is t1 published now or should that ALTER have caused an error?

This won't be allowed either. We don't allow to Add/Drop from All
Tables publication unless the user performs a RESET. This is true even
today except that we don't have a RESET syntax.

~~

It feels like there are too many tricky rules when using EXCEPT with
ALTER PUBLICATION. I guess complexities can be described in the
documentation but IMO it would be better if the ALTER syntax could be
unambiguous in the first place.

Agreed.

So perhaps the rules should be more
restrictive (e.g. just disallow ALTER ... ADD any table that overlaps
the existing EXCEPT list ??)

I think the current proposal seems to be restrictive enough to avoid
any tricky issues. Do you see any other problem?

--
With Regards,
Amit Kapila.

#26Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#12)
Re: Skipping schema changes in publication

On 14.04.22 15:47, Peter Eisentraut wrote:

That said, I'm not sure this feature is worth the trouble.  If this is
useful, what about "whole database except these schemas"?  What about
"create this database from this template except these schemas".  This
could get out of hand.  I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

Another problem in general with this "all except these" way of
specifying things is that you need to track negative dependencies.

For example, assume you can't add a table to a publication unless it has
a replica identity. Now, if you have a publication p1 that says
includes "all tables except t1", you now have to check p1 whenever a new
table is created, even though the new table has no direct dependency
link with p1. So in more general cases, you would have to check all
existing objects to see whether their specification is in conflict with
the new object being created.

Now publications don't actually work that way, so it's not a real
problem right now, but similar things could work like that. So I think
it's worth thinking this through a bit.

#27Amit Kapila
amit.kapila16@gmail.com
In reply to: Peter Eisentraut (#26)
Re: Skipping schema changes in publication

On Wed, May 4, 2022 at 7:05 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 14.04.22 15:47, Peter Eisentraut wrote:

That said, I'm not sure this feature is worth the trouble. If this is
useful, what about "whole database except these schemas"? What about
"create this database from this template except these schemas". This
could get out of hand. I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

Another problem in general with this "all except these" way of
specifying things is that you need to track negative dependencies.

For example, assume you can't add a table to a publication unless it has
a replica identity. Now, if you have a publication p1 that says
includes "all tables except t1", you now have to check p1 whenever a new
table is created, even though the new table has no direct dependency
link with p1. So in more general cases, you would have to check all
existing objects to see whether their specification is in conflict with
the new object being created.

Yes, I think we should avoid adding such negative dependencies. We
have carefully avoided such dependencies during row filter, column
list work where we don't try to perform DDL time verification.
However, it is not clear to me how this proposal is related to this
example or in general about tracking negative dependencies? AFAIR, we
currently have such a check while changing persistence of logged table
(logged to unlogged, see ATPrepChangePersistence) where we cannot
allow changing persistence if that relation is part of some
publication. But as per my understanding, this feature shouldn't add
any such new dependencies. I agree that we have to ensure that
existing checks shouldn't break due to this feature.

Now publications don't actually work that way, so it's not a real
problem right now, but similar things could work like that. So I think
it's worth thinking this through a bit.

This is a good point and I agree that we should be careful to not add
some new negative dependencies unless it is really required but I
can't see how this proposal will make it more prone to such checks.

--
With Regards,
Amit Kapila.

#28Amit Kapila
amit.kapila16@gmail.com
In reply to: Amit Kapila (#27)
Re: Skipping schema changes in publication

On Thu, May 5, 2022 at 9:20 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, May 4, 2022 at 7:05 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 14.04.22 15:47, Peter Eisentraut wrote:

That said, I'm not sure this feature is worth the trouble. If this is
useful, what about "whole database except these schemas"? What about
"create this database from this template except these schemas". This
could get out of hand. I think we should encourage users to group their
object the way they want and not offer these complicated negative
selection mechanisms.

Another problem in general with this "all except these" way of
specifying things is that you need to track negative dependencies.

For example, assume you can't add a table to a publication unless it has
a replica identity. Now, if you have a publication p1 that says
includes "all tables except t1", you now have to check p1 whenever a new
table is created, even though the new table has no direct dependency
link with p1. So in more general cases, you would have to check all
existing objects to see whether their specification is in conflict with
the new object being created.

Yes, I think we should avoid adding such negative dependencies. We
have carefully avoided such dependencies during row filter, column
list work where we don't try to perform DDL time verification.
However, it is not clear to me how this proposal is related to this
example or in general about tracking negative dependencies?

I mean to say that even if we have such a restriction, it would apply
to "for all tables" or other publications as well. In your example,
consider one wants to Alter a table and remove its replica identity,
we have to check whether the table is part of any publication similar
to what we are doing for relation persistence in
ATPrepChangePersistence.

AFAIR, we
currently have such a check while changing persistence of logged table
(logged to unlogged, see ATPrepChangePersistence) where we cannot
allow changing persistence if that relation is part of some
publication. But as per my understanding, this feature shouldn't add
any such new dependencies. I agree that we have to ensure that
existing checks shouldn't break due to this feature.

Now publications don't actually work that way, so it's not a real
problem right now, but similar things could work like that. So I think
it's worth thinking this through a bit.

This is a good point and I agree that we should be careful to not add
some new negative dependencies unless it is really required but I
can't see how this proposal will make it more prone to such checks.

--
With Regards,
Amit Kapila.

#29Peter Smith
smithpb2250@gmail.com
In reply to: Amit Kapila (#22)
Re: Skipping schema changes in publication

On Thu, Apr 28, 2022 at 9:32 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

...

Another idea that occurred to me today for tables this is as follows:
1. Allow to mention except during create publication ... For All Tables.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
2. Allow to Reset it. This new syntax will reset all objects in the
publications.
Alter Publication ... RESET;
3. Allow to add it to an existing publication
Alter Publication ... Add ALL TABLES [EXCEPT TABLE t1,t2];

I think it can be extended in a similar way for schema syntax as well.

If the proposed syntax ALTER PUBLICATION ... RESET will reset all the
objects in the publication then there still seems simple way to remove
only the EXCEPT list but leave everything else intact. IIUC to clear
just the EXCEPT list would require a 2 step process - 1) ALTER ...
RESET then 2) ALTER ... ADD ALL TABLES again.

I was wondering if it might be useful to have a variation that *only*
resets the EXCEPT list, but still leaves everything else as-is?

So, instead of:
ALTER PUBLICATION pubname RESET

use a syntax something like:
ALTER PUBLICATION pubname RESET {ALL | EXCEPT}
or
ALTER PUBLICATION pubname RESET [EXCEPT]

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

#30vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#29)
Re: Skipping schema changes in publication

On Fri, May 6, 2022 at 8:05 AM Peter Smith <smithpb2250@gmail.com> wrote:

On Thu, Apr 28, 2022 at 9:32 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

...

Another idea that occurred to me today for tables this is as follows:
1. Allow to mention except during create publication ... For All Tables.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
2. Allow to Reset it. This new syntax will reset all objects in the
publications.
Alter Publication ... RESET;
3. Allow to add it to an existing publication
Alter Publication ... Add ALL TABLES [EXCEPT TABLE t1,t2];

I think it can be extended in a similar way for schema syntax as well.

If the proposed syntax ALTER PUBLICATION ... RESET will reset all the
objects in the publication then there still seems simple way to remove
only the EXCEPT list but leave everything else intact. IIUC to clear
just the EXCEPT list would require a 2 step process - 1) ALTER ...
RESET then 2) ALTER ... ADD ALL TABLES again.

I was wondering if it might be useful to have a variation that *only*
resets the EXCEPT list, but still leaves everything else as-is?

So, instead of:
ALTER PUBLICATION pubname RESET

+1 for this syntax as this syntax can be extendable to include options
like (except/all/etc) later.
Currently we can support this syntax and can be extended later based
on the requirements.

The new feature will handle the various use cases based on the
behavior given below:
-- CREATE Publication with EXCEPT TABLE syntax
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2; -- ok
Alter Publication pub1 RESET;
-- All Tables and options are reset similar to creating publication
without any publication object and publication option (create
publication pub1)
\dRp+ pub1
Publication pub2
Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
vignesh | f | t | t | t | t | f
(1 row)

-- Can add except table after reset of publication
ALTER PUBLICATION pub1 Add ALL TABLES EXCEPT TABLE t1,t2; -- ok

-- Cannot add except table without reset of publication
ALTER PUBLICATION pub1 Add EXCEPT TABLE t3,t4; -- not ok, need to be reset

Alter Publication pub1 RESET;
-- Cannot add table to ALL TABLES Publication
ALTER PUBLICATION pub1 Add ALL TABLES EXCEPT TABLE t1,t2, t3, t4,
TABLE t5; -- not ok, ALL TABLES Publications does not support
including of TABLES

Alter Publication pub1 RESET;
-- Cannot add table to ALL TABLES Publication
ALTER PUBLICATION pub1 Add ALL TABLES TABLE t1,t2; -- not ok, ALL
TABLES Publications does not support including of TABLES

-- Cannot add ALL TABLES IN SCHEMA to ALL TABLES Publication
ALTER PUBLICATION pub1 Add ALL TABLES ALL TABLES IN SCHEMA sch1, sch2;
-- not ok, ALL TABLES Publications does not support including of ALL
TABLES IN SCHEMA

-- Existing syntax should work as it is
CREATE PUBLICATION pub1 FOR TABLE t1;
ALTER PUBLICATION pub1 ADD TABLE t1; -- ok, existing ALTER should work
as it is (ok without reset)
ALTER PUBLICATION pub1 ADD ALL TABLES IN SCHEMA sch1; -- ok, existing
ALTER should work as it is (ok without reset)
ALTER PUBLICATION pub1 DROP TABLE t1; -- ok, existing ALTER should
work as it is (ok without reset)
ALTER PUBLICATION pub1 DROP ALL TABLES IN SCHEMA sch1; -- ok, existing
ALTER should work as it is (ok without reset)
ALTER PUBLICATION pub1 SET TABLE t1; -- ok, existing ALTER should work
as it is (ok without reset)
ALTER PUBLICATION pub1 SET ALL TABLES IN SCHEMA sch1; -- ok, existing
ALTER should work as it is (ok without reset)

I will modify the patch to handle this.

Regards,
Vignesh

#31vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#30)
1 attachment(s)
Re: Skipping schema changes in publication

On Tue, May 10, 2022 at 9:08 AM vignesh C <vignesh21@gmail.com> wrote:

On Fri, May 6, 2022 at 8:05 AM Peter Smith <smithpb2250@gmail.com> wrote:

On Thu, Apr 28, 2022 at 9:32 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

...

Another idea that occurred to me today for tables this is as follows:
1. Allow to mention except during create publication ... For All Tables.
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
2. Allow to Reset it. This new syntax will reset all objects in the
publications.
Alter Publication ... RESET;
3. Allow to add it to an existing publication
Alter Publication ... Add ALL TABLES [EXCEPT TABLE t1,t2];

I think it can be extended in a similar way for schema syntax as well.

If the proposed syntax ALTER PUBLICATION ... RESET will reset all the
objects in the publication then there still seems simple way to remove
only the EXCEPT list but leave everything else intact. IIUC to clear
just the EXCEPT list would require a 2 step process - 1) ALTER ...
RESET then 2) ALTER ... ADD ALL TABLES again.

I was wondering if it might be useful to have a variation that *only*
resets the EXCEPT list, but still leaves everything else as-is?

So, instead of:
ALTER PUBLICATION pubname RESET

+1 for this syntax as this syntax can be extendable to include options
like (except/all/etc) later.
Currently we can support this syntax and can be extended later based
on the requirements.

The attached patch has the implementation for "ALTER PUBLICATION
pubname RESET". This command will reset the publication to default
state which includes resetting the publication options, setting ALL
TABLES option to false and dropping the relations and schemas that are
associated with the publication.

Regards,
Vignesh

Attachments:

v1-0001-Add-RESET-option-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Add-RESET-option-to-Alter-Publication-which-will-.patchDownload
From c55befe6f53649babce1dd526b1c123b77731dcd Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Thu, 12 May 2022 08:29:38 +0530
Subject: [PATCH v1] Add RESET option to Alter Publication which will reset the
 publication with default values.

This patch adds a new RESET option to ALTER PUBLICATION which will reset
the publication to default state which includes resetting the publication
options, setting ALL TABLES option to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   | 29 +++++--
 src/backend/commands/publicationcmds.c    | 99 +++++++++++++++++++++++
 src/backend/parser/gram.y                 |  9 +++
 src/bin/psql/tab-complete.c               |  2 +-
 src/include/nodes/parsenodes.h            |  3 +-
 src/test/regress/expected/publication.out | 69 ++++++++++++++++
 src/test/regress/sql/publication.sql      | 37 +++++++++
 7 files changed, 241 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..29f3858de1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,7 +66,18 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to default
+   state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> option to <literal>false</literal> and drop the
+   relations and schemas that are associated with the publication.
   </para>
 
   <para>
@@ -73,10 +85,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    Adding a table to a publication additionally requires owning that table.
    The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires invoking user to be a superuser. To alter the owner, you must also
+   be a direct or indirect member of the new owning role. The new owner must
+   have <literal>CREATE</literal> privilege on the database.  Also, the new
+   owner of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
    SCHEMA</literal> publication must be a superuser. However, a superuser can
    change the ownership of a publication regardless of these restrictions.
   </para>
@@ -207,6 +220,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Resetting the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6df0e6670f..c883c4f75a 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1104,6 +1104,103 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and publication schemas.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+						Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Check and reset the options */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(false);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+	}
+
+	if (!pubform->pubinsert)
+	{
+		values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(true);
+		replaces[Anum_pg_publication_pubinsert - 1] = true;
+	}
+
+	if (!pubform->pubupdate)
+	{
+		values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(true);
+		replaces[Anum_pg_publication_pubupdate - 1] = true;
+	}
+
+	if (!pubform->pubdelete)
+	{
+		values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(true);
+		replaces[Anum_pg_publication_pubdelete - 1] = true;
+	}
+
+	if (!pubform->pubtruncate)
+	{
+		values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(true);
+		replaces[Anum_pg_publication_pubtruncate - 1] = true;
+	}
+
+	if (pubform->pubviaroot)
+	{
+		values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(false);
+		replaces[Anum_pg_publication_pubviaroot - 1] = true;
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							RelationGetRelationName(rel))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1415,6 +1512,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ReSetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4..755a861613 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10005,6 +10005,8 @@ pub_obj_list: 	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10047,6 +10049,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *)n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ReSetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 588c0841fe..232f56a01c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1819,7 +1819,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9a716f3794..ac30c4f6c8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4033,7 +4033,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ReSetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 398c0f38f6..dc89094e08 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,75 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Test for RESET PUBLICATION
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' option is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that publish option and publish via root option is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can execute RESET publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..696f723da6 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,43 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Test for RESET PUBLICATION
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' option is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that publish option and publish via root option is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can execute RESET publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

#32Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#31)
Re: Skipping schema changes in publication

On Thu, May 12, 2022 at 2:24 PM vignesh C <vignesh21@gmail.com> wrote:

...

The attached patch has the implementation for "ALTER PUBLICATION
pubname RESET". This command will reset the publication to default
state which includes resetting the publication options, setting ALL
TABLES option to false and dropping the relations and schemas that are
associated with the publication.

Please see below my review comments for the v1-0001 (RESET) patch

======

1. Commit message

This patch adds a new RESET option to ALTER PUBLICATION which

Wording: "RESET option" -> "RESET clause"

~~~

2. doc/src/sgml/ref/alter_publication.sgml

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to default
+   state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> option to <literal>false</literal>
and drop the
+   relations and schemas that are associated with the publication.
   </para>

2a. Wording: "to default state" -> "to the default state"

2b. Wording: "and drop the relations..." -> "and dropping all relations..."

~~~

3. doc/src/sgml/ref/alter_publication.sgml

+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires invoking user to be a superuser. To alter the owner, you must also

Wording: "requires invoking user" -> "requires the invoking user"

~~~

4. doc/src/sgml/ref/alter_publication.sgml - Example

@@ -207,6 +220,12 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users,
departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Resetting the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;

Wording: "Resetting the publication" -> "Reset the publication"

~~~

5. src/backend/commands/publicationcmds.c

+ /* Check and reset the options */

IMO the code can just reset all these options unconditionally. I did
not see the point to check for existing option values first. I feel
the simpler code outweighs any negligible performance difference in
this case.

~~~

6. src/backend/commands/publicationcmds.c

+ /* Check and reset the options */

Somehow it seemed a pity having to hardcode all these default values
true/false in multiple places; e.g. the same is already hardcoded in
the parse_publication_options function.

To avoid multiple hard coded bools you could just call the
parse_publication_options with an empty options list. That would set
the defaults which you can then use:
values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(pubactiondefs->insert);

Alternatively, maybe there should be #defines to use instead of having
the scattered hardcoded bool defaults:
#define PUBACTION_DEFAULT_INSERT true
#define PUBACTION_DEFAULT_UPDATE true
etc

~~~

7. src/include/nodes/parsenodes.h

@@ -4033,7 +4033,8 @@ typedef enum AlterPublicationAction
 {
  AP_AddObjects, /* add objects to publication */
  AP_DropObjects, /* remove objects from publication */
- AP_SetObjects /* set list of objects */
+ AP_SetObjects, /* set list of objects */
+ AP_ReSetPublication /* reset the publication */
 } AlterPublicationAction;

Unusual case: "AP_ReSetPublication" -> "AP_ResetPublication"

~~~

8. src/test/regress/sql/publication.sql

8a.
+-- Test for RESET PUBLICATION
SUGGESTED
+-- Tests for ALTER PUBLICATION ... RESET
8b.
+-- Verify that 'ALL TABLES' option is reset
SUGGESTED:
+-- Verify that 'ALL TABLES' flag is reset
8c.
+-- Verify that publish option and publish via root option is reset
SUGGESTED:
+-- Verify that publish options and publish_via_partition_root option are reset
8d.
+-- Verify that only superuser can execute RESET publication
SUGGESTED
+-- Verify that only superuser can reset a publication

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

#33vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#32)
2 attachment(s)
Re: Skipping schema changes in publication

On Fri, May 13, 2022 at 9:37 AM Peter Smith <smithpb2250@gmail.com> wrote:

On Thu, May 12, 2022 at 2:24 PM vignesh C <vignesh21@gmail.com> wrote:

...

The attached patch has the implementation for "ALTER PUBLICATION
pubname RESET". This command will reset the publication to default
state which includes resetting the publication options, setting ALL
TABLES option to false and dropping the relations and schemas that are
associated with the publication.

Please see below my review comments for the v1-0001 (RESET) patch

======

1. Commit message

This patch adds a new RESET option to ALTER PUBLICATION which

Wording: "RESET option" -> "RESET clause"

Modified

~~~

2. doc/src/sgml/ref/alter_publication.sgml

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to default
+   state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> option to <literal>false</literal>
and drop the
+   relations and schemas that are associated with the publication.
</para>

2a. Wording: "to default state" -> "to the default state"

Modified

2b. Wording: "and drop the relations..." -> "and dropping all relations..."

Modified

~~~

3. doc/src/sgml/ref/alter_publication.sgml

+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires invoking user to be a superuser. To alter the owner, you must also

Wording: "requires invoking user" -> "requires the invoking user"

Modified

~~~

4. doc/src/sgml/ref/alter_publication.sgml - Example

@@ -207,6 +220,12 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
<structname>production_publication</structname>:
<programlisting>
ALTER PUBLICATION production_publication ADD TABLE users,
departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Resetting the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;

Wording: "Resetting the publication" -> "Reset the publication"

Modified

~~~

5. src/backend/commands/publicationcmds.c

+ /* Check and reset the options */

IMO the code can just reset all these options unconditionally. I did
not see the point to check for existing option values first. I feel
the simpler code outweighs any negligible performance difference in
this case.

Modified

~~~

6. src/backend/commands/publicationcmds.c

+ /* Check and reset the options */

Somehow it seemed a pity having to hardcode all these default values
true/false in multiple places; e.g. the same is already hardcoded in
the parse_publication_options function.

To avoid multiple hard coded bools you could just call the
parse_publication_options with an empty options list. That would set
the defaults which you can then use:
values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(pubactiondefs->insert);

Alternatively, maybe there should be #defines to use instead of having
the scattered hardcoded bool defaults:
#define PUBACTION_DEFAULT_INSERT true
#define PUBACTION_DEFAULT_UPDATE true
etc

I have used #define for default value and used it in both the functions.

~~~

7. src/include/nodes/parsenodes.h

@@ -4033,7 +4033,8 @@ typedef enum AlterPublicationAction
{
AP_AddObjects, /* add objects to publication */
AP_DropObjects, /* remove objects from publication */
- AP_SetObjects /* set list of objects */
+ AP_SetObjects, /* set list of objects */
+ AP_ReSetPublication /* reset the publication */
} AlterPublicationAction;

Unusual case: "AP_ReSetPublication" -> "AP_ResetPublication"

Modified

~~~

8. src/test/regress/sql/publication.sql

8a.
+-- Test for RESET PUBLICATION
SUGGESTED
+-- Tests for ALTER PUBLICATION ... RESET

Modified

8b.
+-- Verify that 'ALL TABLES' option is reset
SUGGESTED:
+-- Verify that 'ALL TABLES' flag is reset

Modified

8c.
+-- Verify that publish option and publish via root option is reset
SUGGESTED:
+-- Verify that publish options and publish_via_partition_root option are reset

Modified

8d.
+-- Verify that only superuser can execute RESET publication
SUGGESTED
+-- Verify that only superuser can reset a publication

Modified

Thanks for the comments, the attached v5 patch has the changes for the
same. Also I have made the changes for SKIP Table based on the new
syntax, the changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Regards,
Vignesh

Attachments:

v5-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v5-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From b38b5fa76c88c2d2df6abf46a760a9422072c989 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v5 1/2] Add RESET clause to Alter Publication which will reset
  the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to default state which includes resetting the publication
options, setting ALL TABLES option to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   | 33 ++++++--
 src/backend/commands/publicationcmds.c    | 98 +++++++++++++++++++++--
 src/backend/parser/gram.y                 |  9 +++
 src/bin/psql/tab-complete.c               |  2 +-
 src/include/nodes/parsenodes.h            |  3 +-
 src/test/regress/expected/publication.out | 69 ++++++++++++++++
 src/test/regress/sql/publication.sql      | 37 +++++++++
 7 files changed, 237 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..189727af62 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,7 +66,18 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> option to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
@@ -73,12 +85,13 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    Adding a table to a publication additionally requires owning that table.
    The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires the invoking user to be a superuser. To alter the owner, you must
+   also be a direct or indirect member of the new owning role. The new owner
+   must have <literal>CREATE</literal> privilege on the database.  Also, the
+   new owner of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES
+   IN SCHEMA</literal> publication must be a superuser. However, a superuser
+   can change the ownership of a publication regardless of these restrictions.
   </para>
 
   <para>
@@ -207,6 +220,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 8e645741e4..956b02d501 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,13 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+#define PUB_ATION_INSERT_DEFAULT true
+#define PUB_ACTION_UPDATE_DEFAULT true
+#define PUB_ACTION_DELETE_DEFAULT true
+#define PUB_ACTION_TRUNCATE_DEFAULT true
+#define PUB_VIA_ROOT_DEFAULT false
+#define PUB_ALL_TABLES_DEFAULT false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +98,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_ATION_INSERT_DEFAULT;
+	pubactions->pubupdate = PUB_ACTION_UPDATE_DEFAULT;
+	pubactions->pubdelete = PUB_ACTION_DELETE_DEFAULT;
+	pubactions->pubtruncate = PUB_ACTION_TRUNCATE_DEFAULT;
+	*publish_via_partition_root = PUB_VIA_ROOT_DEFAULT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1112,85 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and publication schemas.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+						Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication options */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_ATION_INSERT_DEFAULT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_ACTION_UPDATE_DEFAULT);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_ACTION_DELETE_DEFAULT);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_ACTION_TRUNCATE_DEFAULT);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_VIA_ROOT_DEFAULT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_ALL_TABLES_DEFAULT);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							RelationGetRelationName(rel))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1502,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 989db0dbec..d7e13666a2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10558,6 +10558,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10604,6 +10606,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 55af9eb04e..62ecc3cdab 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1819,7 +1819,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 73f635b455..9726fdae58 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4035,7 +4035,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 398c0f38f6..f8527dae02 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,75 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..0612315488 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,43 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From cf0e031f8f82541bbd10bd4963e7da051e17a022 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:22:11 +0530
Subject: [PATCH v5 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new option "EXCEPT TABLE" in Create/Alter Publication allows
one or more tables to be excluded, publisher will exclude sending the data
of the excluded tables to the subscriber.

The new syntax allows specifying exclude relations while creating a publication
or exclude relations in alter publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column prexcept is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude publishing through the publication.
Modified the output plugin (pgoutput) to exclude publishing the changes of the
excluded tables.

Updates pg_dump to identify and dump the excluded tables of the publications.
Updates the \d family of commands to display excluded tables of the
publications and \dRp+ variant will now display associated except tables if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   5 +-
 doc/src/sgml/ref/alter_publication.sgml       |  13 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  38 +++-
 src/backend/commands/publicationcmds.c        | 196 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  48 ++++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  61 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  12 ++
 src/bin/psql/describe.c                       |  56 ++++-
 src/bin/psql/tab-complete.c                   |  12 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   4 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  90 +++++++-
 src/test/regress/sql/publication.sql          |  54 ++++-
 .../t/032_rep_changes_except_table.pl         |  86 ++++++++
 24 files changed, 660 insertions(+), 122 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a533a2153e..78e8c22a59 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the table must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..d7d6ba0529 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   <para>
    To add tables to a publication, the user must have ownership rights on the
    table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 189727af62..e895add262 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]]
 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>] [, ... ] )
@@ -82,8 +83,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
+   Adding a table or excluding a table to a publication additionally requires
+   owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
    invoking user to be a superuser.  <literal>RESET</literal> of publication
    requires the invoking user to be a superuser. To alter the owner, you must
@@ -213,6 +214,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> that
+   publishes all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT TABLE users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..9ce6bc7e37 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -156,6 +156,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +369,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..3889796b3f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index e2c8bcb279..d84aad526e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = GetRelationPublications(ancestor, false);
 		List	   *aschemaPubids = NIL;
+		List	   *aexceptpubids = NIL;
 
 		level++;
 
@@ -317,7 +319,9 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		else
 		{
 			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			aexceptpubids = GetRelationPublications(ancestor, true);
+			if (list_member_oid(aschemaPubids, puboid) ||
+				(puballtables && !list_member_oid(aexceptpubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
@@ -328,6 +332,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 
 		list_free(apubids);
 		list_free(aschemaPubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +401,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -666,7 +673,7 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 
 /* Gets list of publication oids for a relation */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bexcept)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +687,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (bexcept == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,7 +787,7 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
@@ -787,6 +795,15 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 	HeapTuple	tuple;
 	List	   *result = NIL;
 
+	/*
+	 * pg_publication_rel and pg_publication_namespace will only have excluded
+	 * tables in case of all tables publication, no need to pass except flag
+	 * to get the relations.
+	 */
+	List	   *exceptpubtablelist;
+
+	exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -802,7 +819,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptpubtablelist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +841,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptpubtablelist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1107,7 +1126,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 956b02d501..ca6edbd397 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -192,6 +192,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -304,7 +309,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -331,7 +336,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -380,7 +386,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -399,7 +405,7 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -843,54 +849,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								&schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+												PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1191,6 +1195,76 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * 	Publication is having default options
+ *  Publication is not associated with relations
+ *  Publication is not associated with schemas
+ *  Publication is not set with "FOR ALL TABLES"
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables)
+		return false;
+
+	if (!pubform->pubinsert || !pubform->pubupdate || !pubform->pubdelete ||
+		!pubform->pubtruncate || pubform->pubviaroot)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and publication schemas.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	Assert(!pubform->puballtables);
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* set all tables option */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1442,6 +1516,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 					  List *tables, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	ListCell   *lc;
+	bool		nonexcepttable = false;
+	bool		excepttable = false;
+
+	foreach(lc, tables)
+	{
+		PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+		if (!pub_table->except)
+			nonexcepttable = true;
+		else
+			excepttable = true;
+	}
 
 	if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) &&
 		schemaidlist && !superuser())
@@ -1461,12 +1548,19 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup,
 				 errdetail("Tables from schema cannot be added to, dropped from, or set on FOR ALL TABLES publications.")));
 
 	/* Check that user is allowed to manipulate the publication tables. */
-	if (tables && pubform->puballtables)
+	if (nonexcepttable && tables && pubform->puballtables)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("publication \"%s\" is defined as FOR ALL TABLES",
 						NameStr(pubform->pubname)),
 				 errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications.")));
+
+	if (excepttable && !stmt->for_all_tables)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+						NameStr(pubform->pubname)),
+				 errdetail("except table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));
 }
 
 /*
@@ -1500,6 +1594,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("Setting ALL TABLES requires publication \"%s\" to have default values",
+						   stmt->pubname),
+					errhint("Either the publication has tables/schemas associated or does not have default publication options or ALL TABLES option is set."));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1745,6 +1853,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1817,6 +1926,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1892,8 +2002,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..5d97eadf54 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16276,7 +16276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16413,7 +16413,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7e13666a2..b6ead7da4b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,7 +455,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -588,6 +588,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT TABLE table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10430,12 +10431,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10473,6 +10475,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10548,6 +10551,34 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			EXCEPT TABLE relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = $1;
+					$$->pubtable->relation = $3;
+					$$->location = @1;
+				}
+			| relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+ except_pub_obj_list:	ExceptPublicationObjSpec
+					{ $$ = list_make1($1); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10586,6 +10617,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 42c06af239..6394466dab 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1996,7 +1996,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2085,22 +2086,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2119,7 +2104,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2134,6 +2120,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2209,6 +2197,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 60e72f9e8b..4659c766dc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5561,6 +5561,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5594,7 +5596,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5608,14 +5610,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5644,7 +5651,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5661,7 +5668,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7cc9c72e49..4ec4c35caa 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -3980,8 +3982,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool first = true;
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			PublicationInfo *relpubinfo = pubrinfo->publication;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == relpubinfo)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE ONLY");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, " %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4151,6 +4179,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4191,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							"SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4212,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4228,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4240,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char       *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4261,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4259,6 +4304,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -4330,8 +4378,11 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
 					  fmtId(pubinfo->dobj.name));
+
+	appendPQExpBufferStr(query, "TABLE ONLY");
+
 	appendPQExpBuffer(query, " %s",
 					  fmtQualifiedDumpable(tbinfo));
 
@@ -9936,6 +9987,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17869,6 +17923,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1f08716f69..13a3b3f875 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,18 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1a5d924a23..f0ef41cb68 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2950,17 +2950,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
-								  "UNION\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+				appendPQExpBuffer(&buf, "UNION\n"
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "		AND NOT EXISTS (SELECT 1\n"
+									  "							FROM pg_catalog.pg_publication_rel pr\n"
+									  "								JOIN pg_catalog.pg_class pc\n"
+									  "	  	 						ON pr.prrelid = pc.oid\n"
+									  "							WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6319,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND pr.prexcept = 'f'\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6322,6 +6344,22 @@ describePublications(const char *pattern)
 			}
 		}
 
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (pset.sversion >= 150000)
+		{
+			/* Get the excluded tables for the specified publication */
+			printfPQExpBuffer(&buf,
+							  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+							  "FROM pg_catalog.pg_class c\n"
+							  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+							  "WHERE pr.prpubid = '%s'\n"
+							  "  AND pr.prexcept = 't'\n"
+							  "ORDER BY 1", pubid);
+			if (!addFooterToPublicationDesc(&buf, "Except tables:",
+											true, &cont))
+				goto error_return;
+		}
+
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62ecc3cdab..3cb512d12c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,9 +1822,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			  HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2986,7 +2990,9 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48205ba429..b6a7e53bf8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool bexcept);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..595c87c540 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* except the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..a515cdb802 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,8 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot, bool alltables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors, bool pubviaroot, bool alltables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9726fdae58..134ad05936 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* except relation */
 } PublicationTable;
 
 /*
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* An Except table */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index f8527dae02..94adb2b873 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,8 +165,19 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
@@ -1659,6 +1670,10 @@ CREATE TABLE pub_sch1.tbl1 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- can't add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Either the publication has tables/schemas associated or does not have default publication options or ALL TABLES option is set.
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,9 +1690,23 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Either the publication has tables/schemas associated or does not have default publication options or ALL TABLES option is set.
+-- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
@@ -1685,6 +1714,26 @@ ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA pu
  regress_publication_user | f          | t       | t       | t       | t         | f
 Tables:
     "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- can't add except table to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Either the publication has tables/schemas associated or does not have default publication options or ALL TABLES option is set.
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
 Tables from schemas:
     "public"
 
@@ -1696,13 +1745,40 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
--- Verify that publish options and publish_via_partition_root option are reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- can't add except table when the publication options does not have default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Either the publication has tables/schemas associated or does not have default publication options or ALL TABLES option is set.
+-- Verify that publish option is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- can't add except table when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Either the publication has tables/schemas associated or does not have default publication options or ALL TABLES option is set.
+-- Verify that publish_via_partition_root option is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
 --------------------------+------------+---------+---------+---------+-----------+----------
- regress_publication_user | f          | f       | f       | f       | f         | t
+ regress_publication_user | f          | t       | t       | t       | t         | t
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 0612315488..38dc887260 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,8 +89,14 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
@@ -1064,22 +1070,58 @@ SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- can't add except table to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- can't add except table to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- can't add except table to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- can't add except table when the publication options does not have default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that publish option is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- can't add except table when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that publish options and publish_via_partition_root option are reset
+-- Verify that publish_via_partition_root option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..8142779f22
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,86 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is excluded for excluded tables');
+
+# Insert some data into few tables and verify that inserted data is not
+# replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the new table data.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#34osumi.takamichi@fujitsu.com
osumi.takamichi@fujitsu.com
In reply to: vignesh C (#33)
RE: Skipping schema changes in publication

On Saturday, May 14, 2022 10:33 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v5 patch has the changes for the same.
Also I have made the changes for SKIP Table based on the new syntax, the
changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Hi,

Thank you for updating the patch.
I'll share few minor review comments on v5-0001.

(1) doc/src/sgml/ref/alter_publication.sgml

@@ -73,12 +85,13 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    Adding a table to a publication additionally requires owning that table.
    The <literal>ADD ALL TABLES IN SCHEMA</literal> and
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires the invoking user to be a superuser. To alter the owner, you must
...

I suggest to combine the first part of your change with one existing sentence
before your change, to make our description concise.

FROM:
"The <literal>ADD ALL TABLES IN SCHEMA</literal> and
<literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
invoking user to be a superuser. <literal>RESET</literal> of publication
requires the invoking user to be a superuser."

TO:
"The <literal>ADD ALL TABLES IN SCHEMA</literal>,
<literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
<literal>RESET</literal> of publication requires the invoking user to be a superuser."

(2) typo

+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,13 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"

+#define PUB_ATION_INSERT_DEFAULT true
+#define PUB_ACTION_UPDATE_DEFAULT true

Kindly change
FROM:
"PUB_ATION_INSERT_DEFAULT"
TO:
"PUB_ACTION_INSERT_DEFAULT"

(3) src/test/regress/expected/publication.out

+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail

We have "-- fail" for one case in this patch.
On the other hand, isn't better to add "-- ok" (or "-- success") for
other successful statements,
when we consider the entire tests description consistency ?

Best Regards,
Takamichi Osumi

#35osumi.takamichi@fujitsu.com
osumi.takamichi@fujitsu.com
In reply to: vignesh C (#33)
RE: Skipping schema changes in publication

On Saturday, May 14, 2022 10:33 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v5 patch has the changes for the same.
Also I have made the changes for SKIP Table based on the new syntax, the
changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Hi,

Several comments on v5-0002.

(1) One unnecessary space before "except_pub_obj_list" syntax definition

+ except_pub_obj_list:  ExceptPublicationObjSpec
+                                       { $$ = list_make1($1); }
+                       | except_pub_obj_list ',' ExceptPublicationObjSpec
+                                       { $$ = lappend($1, $3); }
+                       |  /*EMPTY*/                                                            { $$ = NULL; }
+       ;
+

From above part, kindly change
FROM:
" except_pub_obj_list: ExceptPublicationObjSpec"
TO:
"except_pub_obj_list: ExceptPublicationObjSpec"

(2) doc/src/sgml/ref/create_publication.sgml

(2-1)

@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]

Here I think we need to add two more whitespaces around square brackets.
Please change
FROM:
"[ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]]"
TO:
"[ FOR ALL TABLES [ EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ] ]"

When I check other documentations, I see whitespaces before/after square brackets.

(2-2)
This whitespace alignment applies to alter_publication.sgml as well.

(3)

@@ -156,6 +156,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
</listitem>
</varlistentry>

+
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+

This EXCEPT TABLE clause is only for FOR ALL TABLES.
So, how about extracting the main message from above part and
moving it to an exising paragraph below, instead of having one independent paragraph ?

<varlistentry>
<term><literal>FOR ALL TABLES</literal></term>
<listitem>
<para>
Marks the publication as one that replicates changes for all tables in
the database, including tables created in the future.
</para>
</listitem>
</varlistentry>

Something like
"Marks the publication as one that replicates changes for all tables in
the database, including tables created in the future. EXCEPT TABLE indicates
excluded tables for the defined publication.
"

(4) One minor confirmation about the syntax

Currently, we allow one way of writing to indicate excluded tables like below.

(example) CREATE PUBLICATION mypub FOR ALL TABLES EXCEPT TABLE tab3, tab4, EXCEPT TABLE tab5;

This is because we define ExceptPublicationObjSpec with EXCEPT TABLE.
Is it OK to have a room to write duplicate "EXCEPT TABLE" clauses ?
I think there is no harm in having this,
but I'd like to confirm whether this syntax might be better to be adjusted or not.

(5) CheckAlterPublication

+
+       if (excepttable && !stmt->for_all_tables)
+               ereport(ERROR,
+                               (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+                                               NameStr(pubform->pubname)),
+                                errdetail("except table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));

Could you please add a test for this ?

Best Regards,
Takamichi Osumi

#36Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#33)
Re: Skipping schema changes in publication

Below are my review comments for v5-0001.

There is some overlap with comments recently posted by Osumi-san [1]/messages/by-id/TYCPR01MB8373C3120C2B3112001ED6F1EDCF9@TYCPR01MB8373.jpnprd01.prod.outlook.com.

(I also have review comments for v5-0002; will post them tomorrow)

======

1. Commit message

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to default state which includes resetting the publication
options, setting ALL TABLES option to false and dropping the relations and
schemas that are associated with the publication.

SUGGEST
"to default state" -> "to the default state"
"ALL TABLES option" -> "ALL TABLES flag"

~~~

2. doc/src/sgml/ref/alter_publication.sgml

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> option to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>

"ALL TABLES option" -> "ALL TABLES flag"

~~~

3. doc/src/sgml/ref/alter_publication.sgml

+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires the invoking user to be a superuser. To alter the owner, you must

SUGGESTION
To <literal>RESET</literal> a publication requires the invoking user
to be a superuser.

~~~

4. src/backend/commands/publicationcmds.c

@@ -53,6 +53,13 @@
#include "utils/syscache.h"
#include "utils/varlena.h"

+#define PUB_ATION_INSERT_DEFAULT true
+#define PUB_ACTION_UPDATE_DEFAULT true
+#define PUB_ACTION_DELETE_DEFAULT true
+#define PUB_ACTION_TRUNCATE_DEFAULT true
+#define PUB_VIA_ROOT_DEFAULT false
+#define PUB_ALL_TABLES_DEFAULT false

4a.
Typo: "ATION" -> "ACTION"

4b.
I think these #defines deserve a 1 line comment.
e.g.
/* CREATE PUBLICATION default values for flags and options */

4c.
Since the "_DEFAULT" is a common part of all the names, maybe it is
tidier if it comes first.
e.g.
#define PUB_DEFAULT_ACTION_INSERT true
#define PUB_DEFAULT_ACTION_UPDATE true
#define PUB_DEFAULT_ACTION_DELETE true
#define PUB_DEFAULT_ACTION_TRUNCATE true
#define PUB_DEFAULT_VIA_ROOT false
#define PUB_DEFAULT_ALL_TABLES false

------
[1]: /messages/by-id/TYCPR01MB8373C3120C2B3112001ED6F1EDCF9@TYCPR01MB8373.jpnprd01.prod.outlook.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#37Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#33)
Re: Skipping schema changes in publication

Below are my review comments for v5-0002.

There may be an overlap with comments recently posted by Osumi-san [1]/messages/by-id/TYCPR01MB83737C28187A6E0BADAE98F0EDCF9@TYCPR01MB8373.jpnprd01.prod.outlook.com.

(I also have review comments for v5-0002; will post them tomorrow)

======

1. General

Is it really necessary to have to say "EXCEPT TABLE" instead of just
"EXCEPT". It seems unnecessarily verbose and redundant when you write
"FOR ALL TABLES EXCEPT TABLE...".

If you want to keep this TABLE keyword (maybe you have plans for other
kinds of except?) then IMO perhaps at least it can be the optional
default except type. e.g. EXCEPT [TABLE].

~~~

2. General

(I was unsure whether to even mention this one).

I understand the "EXCEPT" is chosen as the user-facing syntax, but it
still seems strange when reading the patch to see attribute members
and column names called 'except'. I think the problem is that "except"
is not a verb, so saying except=t/f just does not make much sense.
Sometimes I feel that for all the internal usage
(code/comments/catalog) using "skip" and "skip-list" etc would be a
much better choice of names. OTOH I can see that having consistency
with the outside syntax might also be good. Anyway, please consider -
maybe other people feel the same?

~~~

3. General

The ONLY keyword seems supported by the syntax for tables of the
except-list (more on this in later comments) but:
a) I am not sure if the patch code is accounting for that, and
b) There are no test cases using ONLY.

~~~

4. Commit message

A new option "EXCEPT TABLE" in Create/Alter Publication allows
one or more tables to be excluded, publisher will exclude sending the data
of the excluded tables to the subscriber.

SUGGESTION
A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

~~

5. Commit message

The new syntax allows specifying exclude relations while creating a publication
or exclude relations in alter publication. For example:

SUGGESTION
The new syntax allows specifying excluded relations when creating or
altering a publication. For example:

~~~

6. Commit message

A new column prexcept is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude publishing through the publication.

SUGGESTION
A new column "prexcept" is added to table "pg_publication_rel", to
maintain the relations that the user wants to exclude from the
publications.

~~~

7. Commit message

Modified the output plugin (pgoutput) to exclude publishing the changes of the
excluded tables.

I did not feel it was necessary to say this. It is already said above
that the data is not sent, so that seems enough.

~~~

8. Commit message

Updates pg_dump to identify and dump the excluded tables of the publications.
Updates the \d family of commands to display excluded tables of the
publications and \dRp+ variant will now display associated except tables if any.

SUGGESTION
pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands to display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

~~~

9. doc/src/sgml/catalogs.sgml

@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration
count&gt;</replaceable>:<replaceable>&l
if there is no publication qualifying condition.</para></entry>
</row>

+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the table must be excluded
+      </para></entry>
+     </row>

Other descriptions on this page refer to "relation" instead of
"table". Probably this should do the same to be consistent.

~~~

10. doc/src/sgml/logical-replication.sgml

@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication
origin "pg_16395" during "INSER
   <para>
    To add tables to a publication, the user must have ownership rights on the
    table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
   </para>

It seems like a valid change but how is this related to this EXCEPT
patch. Maybe this fix should be patched separately?

~~~

11. doc/src/sgml/ref/alter_publication.sgml

@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD <replaceable class="parameter">publication_object</replaceable> [,
...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable
class="parameter">table_name</replaceable> [ * ] [, ... ]]

The [ONLY] looks misplaced when the syntax is described like this. For
example, in practice it is possible to write "EXCEPT TABLE ONLY t1,
ONLY t2, t3, ONLY t4" but it doesn't seem that way by looking at these
PG DOCS.

IMO would be better described like this:

[ FOR ALL TABLES [ EXCEPT TABLE exception_object [,...] ]]

where exception_object is:

[ ONLY ] table_name [ * ]

~~~

12. doc/src/sgml/ref/alter_publication.sgml

@@ -82,8 +83,8 @@ ALTER PUBLICATION <replaceable
class="parameter">name</replaceable> RESET

   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
+   Adding a table or excluding a table to a publication additionally requires
+   owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal> and

SUGGESTION
Adding a table to or excluding a table from a publication additionally
requires owning that table.

~~~

13. doc/src/sgml/ref/alter_publication.sgml

@@ -213,6 +214,14 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
</programlisting>
</para>

+  <para>
+   Alter publication <structname>production_publication</structname> that
+   publishes all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>

"that publishes" -> "to publish"

~~~

14. doc/src/sgml/ref/create_publication.sgml

(Same comment about the ONLY syntax as #11)

~~~

15. doc/src/sgml/ref/create_publication.sgml

+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>

IMO you can remove all that "It is not supported for..." sentence. You
don't need to spell that out again when it is already clear from the
syntax.

~~~

16. doc/src/sgml/ref/psql-ref.sgml

@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication
are shown as
+        well.
         </para>

Perhaps this is OK just as-is, but OTOH I felt that the change was
almost unnecessary because saying it displays "the tables" kind of
implies it would also have to account for the "excluded tables" too.

~~~

17. src/backend/catalog/pg_publication.c - GetTopMostAncestorInPublication

@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List
*ancestors, int *ancestor_level
  foreach(lc, ancestors)
  {
  Oid ancestor = lfirst_oid(lc);
- List    *apubids = GetRelationPublications(ancestor);
+ List    *apubids = GetRelationPublications(ancestor, false);
  List    *aschemaPubids = NIL;
+ List    *aexceptpubids = NIL;

17a.
I think the var "aschemaPubids" and "aexceptpubids" are only used in
the 'else' block so it seems better they can be declared and freed in
that block too instead of always.

17b.
Also, the camel-case of those variables is inconsistent so may fix
that at the same time.

~~~

18. src/backend/catalog/pg_publication.c - GetRelationPublications

@@ -666,7 +673,7 @@ publication_add_schema(Oid pubid, Oid schemaid,
bool if_not_exists)

 /* Gets list of publication oids for a relation */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bexcept)

18a.
I felt that "except_flag" is a better name than "bexcept" for this param.

18b.
The function comment should be updated to say only relations matching
this except_flag are returned in the list.

~~~

19. src/backend/catalog/pg_publication.c - GetAllTablesPublicationRelations

@@ -787,6 +795,15 @@ GetAllTablesPublicationRelations(bool pubviaroot)
HeapTuple tuple;
List *result = NIL;

+ /*
+ * pg_publication_rel and pg_publication_namespace will only have excluded
+ * tables in case of all tables publication, no need to pass except flag
+ * to get the relations.
+ */
+ List    *exceptpubtablelist;
+
+ exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+

19a.
I wasn't very sure of the meaning/intent of the comment, but IIUC it
seems to be explaining why it is not necessary to use an "except_flag"
parameter in this code. Is it necessary/helpful to explain parameters
that do NOT exist?

19b.
The var name "exceptpubtablelist" seems a bit overkill. (e.g.
"excepttablelist" or "exceptlist" etc... are shorter but seem equally
informative).

~~~

20. src/backend/commands/publicationcmds.c - CreatePublication

@@ -843,54 +849,52 @@ CreatePublication(ParseState *pstate,
CreatePublicationStmt *stmt)
/* Make the changes visible. */
CommandCounterIncrement();

- /* Associate objects with the publication. */
- if (stmt->for_all_tables)
- {
- /* Invalidate relcache so that publication info is rebuilt. */
- CacheInvalidateRelcacheAll();
- }
- else
- {
- ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-    &schemaidlist);
+ ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+ &schemaidlist);
- /* FOR ALL TABLES IN SCHEMA requires superuser */
- if (list_length(schemaidlist) > 0 && !superuser())
- ereport(ERROR,
- errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+ /* FOR ALL TABLES IN SCHEMA requires superuser */
+ if (list_length(schemaidlist) > 0 && !superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
- if (list_length(relations) > 0)
- {
- List    *rels;
+ if (list_length(relations) > 0)
+ {
+ List    *rels;
- rels = OpenTableList(relations);
- CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-   PUBLICATIONOBJ_TABLE);
+ rels = OpenTableList(relations);
+ CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+ PUBLICATIONOBJ_TABLE);
- TransformPubWhereClauses(rels, pstate->p_sourcetext,
- publish_via_partition_root);
+ TransformPubWhereClauses(rels, pstate->p_sourcetext,
+ publish_via_partition_root);
- CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-    publish_via_partition_root);
+ CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+ publish_via_partition_root);
- PublicationAddTables(puboid, rels, true, NULL);
- CloseTableList(rels);
- }
+ PublicationAddTables(puboid, rels, true, NULL);
+ CloseTableList(rels);
+ }
- if (list_length(schemaidlist) > 0)
- {
- /*
- * Schema lock is held until the publication is created to prevent
- * concurrent schema deletion.
- */
- LockSchemaList(schemaidlist);
- PublicationAddSchemas(puboid, schemaidlist, true, NULL);
- }
+ if (list_length(schemaidlist) > 0)
+ {
+ /*
+ * Schema lock is held until the publication is created to prevent
+ * concurrent schema deletion.
+ */
+ LockSchemaList(schemaidlist);
+ PublicationAddSchemas(puboid, schemaidlist, true, NULL);
  }

table_close(rel, RowExclusiveLock);

+ /* Associate objects with the publication. */
+ if (stmt->for_all_tables)
+ {
+ /* Invalidate relcache so that publication info is rebuilt. */
+ CacheInvalidateRelcacheAll();
+ }
+

This function is refactored a lot to not use "if/else" as it did
before. But AFAIK (maybe I misunderstood) this refactor doesn't seem
to actually have anything to do with the EXCEPT patch. If it really is
unrelated maybe it should not be part of this patch.

~~~

21. src/backend/commands/publicationcmds.c - CheckPublicationDefValues

+ if (pubform->puballtables)
+ return false;
+
+ if (!pubform->pubinsert || !pubform->pubupdate || !pubform->pubdelete ||
+ !pubform->pubtruncate || pubform->pubviaroot)
+ return false;

Now you have all the #define for the PUB_DEFAULT_XXX values, perhaps
this function should be using them instead of the hardcoded
assumptions what the default values are.

e.g.

if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES) return false;
if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT) return false;
...
etc.

~~~

22. src/backend/commands/publicationcmds.c - CheckAlterPublication

@@ -1442,6 +1516,19 @@ CheckAlterPublication(AlterPublicationStmt
*stmt, HeapTuple tup,
    List *tables, List *schemaidlist)
 {
  Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+ ListCell   *lc;
+ bool nonexcepttable = false;
+ bool excepttable = false;
+
+ foreach(lc, tables)
+ {
+ PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+ if (!pub_table->except)
+ nonexcepttable = true;
+ else
+ excepttable = true;
+ }

22a.
The names are very confusing. e.g. "nonexcepttable" is like a double-negative.

SUGGEST:
bool has_tables = false;
bool has_except_tables = false;

22b.
Reverse the "if" condition to be positive instead of negative (remove !)
e.g.
if (pub_table->except)
has_except_table = true;
else
has_table = true;

~~~

23. src/backend/commands/publicationcmds.c - CheckAlterPublication

@@ -1461,12 +1548,19 @@ CheckAlterPublication(AlterPublicationStmt
*stmt, HeapTuple tup,
errdetail("Tables from schema cannot be added to, dropped from, or
set on FOR ALL TABLES publications.")));

  /* Check that user is allowed to manipulate the publication tables. */
- if (tables && pubform->puballtables)
+ if (nonexcepttable && tables && pubform->puballtables)
  ereport(ERROR,

Seems no reason for "tables" to be in the condition since
"nonexcepttable" can't be true if "tables" is NIL.

~~~

24. src/backend/commands/publicationcmds.c - CheckAlterPublication

+
+ if (excepttable && !stmt->for_all_tables)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+ NameStr(pubform->pubname)),
+ errdetail("except table cannot be added to, dropped from, or set on
NON ALL TABLES publications.")));

The errdetail message seems over-complex.

SUGGESTION
"EXCEPT TABLE clause is only allowed for FOR ALL TABLES publications."

~~~

25. src/backend/commands/publicationcmds.c - AlterPublication

@@ -1500,6 +1594,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Either the publication has tables/schemas associated or
does not have default publication options or ALL TABLES option is
set."));

The errhint message seems over-complex.

SUGGESTION
"Use ALTER PUBLICATION ... RESET"

~~~

26. src/bin/pg_dump/pg_dump.c - dumpPublication

@@ -3980,8 +3982,34 @@ dumpPublication(Archive *fout, const
PublicationInfo *pubinfo)
qpubname);

  if (pubinfo->puballtables)
+ {
+ SimplePtrListCell *cell;
+ bool first = true;
  appendPQExpBufferStr(query, " FOR ALL TABLES");
+ /* Include exception tables if the publication has except tables */
+ for (cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ PublicationInfo *relpubinfo = pubrinfo->publication;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == relpubinfo)
+ {
+ tbinfo = pubrinfo->pubtable;
+
+ if (first)
+ {
+ appendPQExpBufferStr(query, " EXCEPT TABLE ONLY");
+ first = false;
+ }
+ else
+ appendPQExpBufferStr(query, ", ");
+ appendPQExpBuffer(query, " %s", fmtQualifiedDumpable(tbinfo));
+ }
+ }
+ }
+

IIUC this usage of ONLY looks incorrect.

26a.
Firstly, if you want to hardwire ONLY then shouldn't it apply to every
of the except-list table, not just the first one? e.g. "EXCEPT TABLE
ONLY t1, ONLY t2, ONLY t3..."

26b.
Secondly, is it even correct to unconditionally hardwire the ONLY? How
do you know that is how the user wanted it?

~~~

27. src/bin/pg_dump/pg_dump.c

@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids
= {NULL, NULL};
static SimpleStringList extension_include_patterns = {NULL, NULL};
static SimpleOidList extension_include_oids = {NULL, NULL};

+static SimplePtrList exceptinfo = {NULL, NULL};
+

Probably I just did not understand how this logic works, but how does
this static work properly if there are multiple publications and 2
different EXCEPT lists? E.g. where is it clearing the "exceptinfo" so
that multiple EXCEPT TABLE lists don't become muddled?

~~~

28. src/bin/pg_dump/pg_dump.c - dumpPublicationTable

@@ -4330,8 +4378,11 @@ dumpPublicationTable(Archive *fout, const
PublicationRelInfo *pubrinfo)

query = createPQExpBuffer();

- appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+ appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
    fmtId(pubinfo->dobj.name));
+
+ appendPQExpBufferStr(query, "TABLE ONLY");
+

That code refactor does not seem necessary for this patch.

~~~

29. src/bin/pg_dump/pg_dump_sort.c

@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
PRIO_FK_CONSTRAINT,
PRIO_POLICY,
PRIO_PUBLICATION,
+ PRIO_PUBLICATION_EXCEPT_REL,
PRIO_PUBLICATION_REL,
PRIO_PUBLICATION_TABLE_IN_SCHEMA,
PRIO_SUBSCRIPTION,

I'm not sure how this enum is used (so perhaps this makes no
difference) but judging by the enum comment why did you put the sort
priority order PRIO_PUBLICATION_EXCEPT_REL before
PRIO_PUBLICATION_REL. Wouldn’t it make more sense the other way
around?

~~~

30. src/bin/psql/describe.c

@@ -2950,17 +2950,34 @@ describeOneTableDetails(const char *schemaname,
    "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
    "        ELSE NULL END) "
    "FROM pg_catalog.pg_publication p\n"
-   "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-   "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-   "WHERE pr.prrelid = '%s'\n"
-   "UNION\n"
+   " JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+   " JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+   "WHERE pr.prrelid = '%s'",
+   oid, oid, oid);

I feel that trailing "\n" ("WHERE pr.prrelid = '%s'\n") should not
have been removed.

~~~

31. src/bin/psql/describe.c

+ /* FIXME: 150000 should be changed to 160000 later for PG16. */
+ if (pset.sversion >= 150000)
+ appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+ appendPQExpBuffer(&buf, "UNION\n"

The "UNION\n" param might be better wrapped onto the next line like it
used to be.

~~~

32. src/bin/psql/describe.c

+ /* FIXME: 150000 should be changed to 160000 later for PG16. */
+ if (pset.sversion >= 150000)
+ appendPQExpBuffer(&buf,
+   " AND NOT EXISTS (SELECT 1\n"
+   " FROM pg_catalog.pg_publication_rel pr\n"
+   " JOIN pg_catalog.pg_class pc\n"
+   "   ON pr.prrelid = pc.oid\n"
+   " WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+   oid);

The whitespace indents in the SQL seem excessive here.

~~~

33. src/bin/psql/describe.c - describePublications

@@ -6322,6 +6344,22 @@ describePublications(const char *pattern)
}
}

+ /* FIXME: 150000 should be changed to 160000 later for PG16. */
+ if (pset.sversion >= 150000)
+ {
+ /* Get the excluded tables for the specified publication */
+ printfPQExpBuffer(&buf,
+   "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+   "FROM pg_catalog.pg_class c\n"
+   "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+   "WHERE pr.prpubid = '%s'\n"
+   "  AND pr.prexcept = 't'\n"
+   "ORDER BY 1", pubid);
+ if (!addFooterToPublicationDesc(&buf, "Except tables:",
+ true, &cont))
+ goto error_return;
+ }
+

I think this code is misplaced. Shouldn't it be if/else and be above
the other 150000 check, otherwise when you change this to PG16 it may
not work as expected?

~~~

34. src/bin/psql/describe.c - describePublications

+ if (!addFooterToPublicationDesc(&buf, "Except tables:",
+ true, &cont))
+ goto error_return;
+ }

Should this be using the _T() macros same as the other prompts for translation?

~~~

35. src/include/catalog/pg_publication.h

I thought the param "bexpect" should be "except_flag".

(same comment as #18a)

~~~

36. src/include/catalog/pg_publication_rel.h

@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
  Oid oid; /* oid */
  Oid prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
  Oid prrelid BKI_LOOKUP(pg_class); /* Oid of the relation */
+ bool prexcept BKI_DEFAULT(f); /* except the relation */

SUGGEST (comment)
/* skip the relation */

~~~

37. src/include/commands/publicationcmds.h

@@ -32,8 +32,8 @@ extern ObjectAddress AlterPublicationOwner(const
char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-    List *ancestors, bool pubviaroot);
+    List *ancestors, bool pubviaroot, bool alltables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
- List *ancestors, bool pubviaroot);
+ List *ancestors, bool pubviaroot, bool alltables);

Elsewhere in this patch, a similarly added param is called
"puballtables" (not "alltables"). Please check all places and use a
consistent param name for all of them.

~~~

38. src/test/regress/sql/publication.sql

There don't seem to be any tests for more than one EXCEPT TABLE (e.g.
no list tests?)

~~~

38. src/test/regress/sql/publication.sql

Maybe adjust all the below comments (a-d) to say "EXCEPT TABLES"
intead of "except tables"

38a.
+-- can't add except table to 'FOR ALL TABLES' publication

38b.
+-- can't add except table to 'FOR TABLE' publication

38c.
+-- can't add except table to 'FOR ALL TABLES IN SCHEMA' publication

38d.
+-- can't add except table when publish_via_partition_root option does not
+-- have default value
38e.
+-- can't add except table when the publication options does not have default
+-- values

SUGGESTION
can't add EXCEPT TABLE when the publication options are not the default values

~~~

39. .../t/032_rep_changes_except_table.pl

39a.
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+ "SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is excluded for excluded tables');

Maybe the "is" message should say "check there is no initial data
copied for the excluded table"

~~~

40 .../t/032_rep_changes_except_table.pl

+# Insert some data into few tables and verify that inserted data is not
+# replicated
+$node_publisher->safe_psql('postgres',
+ "INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");

The comment is not quite correct. You are inserting into only one
table here - not "few tables".

~~~

41. .../t/032_rep_changes_except_table.pl

+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the new table data.

"new table data" -> "changed data for this table"

------
[1]: /messages/by-id/TYCPR01MB83737C28187A6E0BADAE98F0EDCF9@TYCPR01MB8373.jpnprd01.prod.outlook.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#38Amit Kapila
amit.kapila16@gmail.com
In reply to: Peter Smith (#37)
Re: Skipping schema changes in publication

On Tue, May 17, 2022 at 7:35 AM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v5-0002.

There may be an overlap with comments recently posted by Osumi-san [1].

(I also have review comments for v5-0002; will post them tomorrow)

======

1. General

Is it really necessary to have to say "EXCEPT TABLE" instead of just
"EXCEPT". It seems unnecessarily verbose and redundant when you write
"FOR ALL TABLES EXCEPT TABLE...".

If you want to keep this TABLE keyword (maybe you have plans for other
kinds of except?)

I don't think there is an immediate plan but one can imagine using
EXCEPT SCHEMA. Then for column lists, one may want to use the syntax
Create Publication pub1 For Table t1 Except Cols (c1, ..);

then IMO perhaps at least it can be the optional
default except type. e.g. EXCEPT [TABLE].

Yeah, that might be okay, so, even if we plan to extend this in the
future, by default we will consider the list of tables after EXCEPT
but if the user mentions EXCEPT SCHEMA or something else then we can
use a different object. Is that sound okay?

3. General

The ONLY keyword seems supported by the syntax for tables of the
except-list (more on this in later comments) but:
a) I am not sure if the patch code is accounting for that, and
b) There are no test cases using ONLY.

~~~

Isn't it better to map ONLY with the way it can already be specified
in CREATE PUBLICATION? I am not sure what exactly is proposed and what
is your suggestion? Can you please explain if it is different from the
way we use it for CREATE PUBLICATION?

--
With Regards,
Amit Kapila.

#39Peter Smith
smithpb2250@gmail.com
In reply to: Amit Kapila (#38)
Re: Skipping schema changes in publication

On Tue, May 17, 2022 at 1:56 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Tue, May 17, 2022 at 7:35 AM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v5-0002.

There may be an overlap with comments recently posted by Osumi-san [1].

(I also have review comments for v5-0002; will post them tomorrow)

======

1. General

Is it really necessary to have to say "EXCEPT TABLE" instead of just
"EXCEPT". It seems unnecessarily verbose and redundant when you write
"FOR ALL TABLES EXCEPT TABLE...".

If you want to keep this TABLE keyword (maybe you have plans for other
kinds of except?)

I don't think there is an immediate plan but one can imagine using
EXCEPT SCHEMA. Then for column lists, one may want to use the syntax
Create Publication pub1 For Table t1 Except Cols (c1, ..);

then IMO perhaps at least it can be the optional
default except type. e.g. EXCEPT [TABLE].

Yeah, that might be okay, so, even if we plan to extend this in the
future, by default we will consider the list of tables after EXCEPT
but if the user mentions EXCEPT SCHEMA or something else then we can
use a different object. Is that sound okay?

Yes. That is what I meant.

3. General

The ONLY keyword seems supported by the syntax for tables of the
except-list (more on this in later comments) but:
a) I am not sure if the patch code is accounting for that, and
b) There are no test cases using ONLY.

~~~

Isn't it better to map ONLY with the way it can already be specified
in CREATE PUBLICATION? I am not sure what exactly is proposed and what
is your suggestion? Can you please explain if it is different from the
way we use it for CREATE PUBLICATION?

Yes, I am not proposing anything different to how ONLY already works
for published tables. I was only questioning whether the patch behaves
correctly when ONLY is specified for the tables of the EXCEPT list. I
had some doubt about it because there are a few other review comments
I wrote (e.g. in pg_dump.c), and also I did not find any ONLY tests,

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

#40shiy.fnst@fujitsu.com
shiy.fnst@fujitsu.com
In reply to: vignesh C (#33)
RE: Skipping schema changes in publication

On Sat, May 14, 2022 9:33 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v5 patch has the changes for the
same. Also I have made the changes for SKIP Table based on the new
syntax, the changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Thanks for your patch. Here are some comments on v5-0001 patch.

+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							RelationGetRelationName(rel))));

I think the relation in the error message should be the one whose oid is
"relid", instead of relation "rel".

Besides, I think it might be better not to report an error in this case. If
"prid" is invalid, just ignore this relation. Because in RESET cases, we want to
drop all tables in the publications, and there is no specific table.
(If you agree with that, similarly missing_ok should be set to true when calling
PublicationDropSchemas().)

Regards,
Shi yu

#41vignesh C
vignesh21@gmail.com
In reply to: osumi.takamichi@fujitsu.com (#34)
2 attachment(s)
Re: Skipping schema changes in publication

On Mon, May 16, 2022 at 8:32 AM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Saturday, May 14, 2022 10:33 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v5 patch has the changes for the same.
Also I have made the changes for SKIP Table based on the new syntax, the
changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Hi,

Thank you for updating the patch.
I'll share few minor review comments on v5-0001.

(1) doc/src/sgml/ref/alter_publication.sgml

@@ -73,12 +85,13 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
Adding a table to a publication additionally requires owning that table.
The <literal>ADD ALL TABLES IN SCHEMA</literal> and
<literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires the invoking user to be a superuser. To alter the owner, you must
...

I suggest to combine the first part of your change with one existing sentence
before your change, to make our description concise.

FROM:
"The <literal>ADD ALL TABLES IN SCHEMA</literal> and
<literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
invoking user to be a superuser. <literal>RESET</literal> of publication
requires the invoking user to be a superuser."

TO:
"The <literal>ADD ALL TABLES IN SCHEMA</literal>,
<literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
<literal>RESET</literal> of publication requires the invoking user to be a superuser."

Modified

(2) typo

+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,13 @@
#include "utils/syscache.h"
#include "utils/varlena.h"

+#define PUB_ATION_INSERT_DEFAULT true
+#define PUB_ACTION_UPDATE_DEFAULT true

Kindly change
FROM:
"PUB_ATION_INSERT_DEFAULT"
TO:
"PUB_ACTION_INSERT_DEFAULT"

Modified

(3) src/test/regress/expected/publication.out

+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail

We have "-- fail" for one case in this patch.
On the other hand, isn't better to add "-- ok" (or "-- success") for
other successful statements,
when we consider the entire tests description consistency ?

We generally do not mention success comments for all the success cases
as that might be an overkill. I felt it is better to keep it as it is.
Thoughts?

The attached v6 patch has the changes for the same.

Regards,
Vignesh

Attachments:

v6-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 72f81a7c56ef764f13949721e227541acf6b719c Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v6 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
options, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   | 38 ++++++---
 src/backend/commands/publicationcmds.c    | 99 +++++++++++++++++++++--
 src/backend/parser/gram.y                 |  9 +++
 src/bin/psql/tab-complete.c               |  2 +-
 src/include/nodes/parsenodes.h            |  3 +-
 src/test/regress/expected/publication.out | 69 ++++++++++++++++
 src/test/regress/sql/publication.sql      | 37 +++++++++
 7 files changed, 241 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..47bd15f1fa 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -207,6 +221,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 8e645741e4..a397521270 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and options */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,85 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and publication schemas.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+						Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication options */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1503,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 989db0dbec..d7e13666a2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10558,6 +10558,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10604,6 +10606,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 55af9eb04e..62ecc3cdab 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1819,7 +1819,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 73f635b455..9726fdae58 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4035,7 +4035,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 398c0f38f6..f8527dae02 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,75 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..868f1c51b1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,43 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

v6-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 62ccb2f172da0fa0b8c3aa83a8f55faea0ccac6b Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 17 May 2022 11:50:00 +0530
Subject: [PATCH v6 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands to display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   5 +-
 doc/src/sgml/ref/alter_publication.sgml       |  18 +-
 doc/src/sgml/ref/create_publication.sgml      |  28 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  42 +++--
 src/backend/commands/publicationcmds.c        | 178 +++++++++++++-----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  39 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  57 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  12 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     | 117 +++++++++++-
 src/test/regress/sql/publication.sql          |  62 +++++-
 .../t/032_rep_changes_except_table.pl         |  85 +++++++++
 24 files changed, 671 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a533a2153e..db9e6d7501 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..d7d6ba0529 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   <para>
    To add tables to a publication, the user must have ownership rights on the
    table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 47bd15f1fa..81a9e87170 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -214,6 +220,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT TABLE users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..0f5eacc711 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,15 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+
+   <varlistentry>
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +366,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..3889796b3f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index e2c8bcb279..eaafc38c07 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,8 +303,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
 
 		level++;
 
@@ -316,18 +316,25 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		}
 		else
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			List	   *aschemapubids = NIL;
+			List	   *aexceptpubids = NIL;
+
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+			aexceptpubids = GetRelationPublications(ancestor, true);
+			if (list_member_oid(aschemapubids, puboid) ||
+				(puballtables && !list_member_oid(aexceptpubids, puboid)))
 			{
 				topmost_relid = ancestor;
 
 				if (ancestor_level)
 					*ancestor_level = level;
 			}
+
+			list_free(aschemapubids);
+			list_free(aexceptpubids);
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +403,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +673,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +689,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +789,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +815,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +837,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1107,7 +1122,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a397521270..8fe81d1707 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								&schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+												PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1192,6 +1197,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * 	Publication is having default options
+ *  Publication is not associated with relations
+ *  Publication is not associated with schemas
+ *  Publication is not set with "FOR ALL TABLES"
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and publication schemas.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	Assert(!pubform->puballtables);
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* set all tables option */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("Setting ALL TABLES requires publication \"%s\" to have default values",
+						   stmt->pubname),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1746,6 +1838,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1818,6 +1911,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1893,8 +1987,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..5d97eadf54 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16276,7 +16276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16413,7 +16413,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7e13666a2..e21985d4cb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,7 +455,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -588,6 +588,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT TABLE table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10430,12 +10431,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10473,6 +10475,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10548,6 +10551,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10586,6 +10608,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 42c06af239..6394466dab 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1996,7 +1996,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2085,22 +2086,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2119,7 +2104,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2134,6 +2120,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2209,6 +2197,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 60e72f9e8b..4659c766dc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5561,6 +5561,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5594,7 +5596,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5608,14 +5610,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5644,7 +5651,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5661,7 +5668,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7cc9c72e49..75a93f0de2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -3980,8 +3982,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool first = true;
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			PublicationInfo *relpubinfo = pubrinfo->publication;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == relpubinfo)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4151,6 +4179,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4191,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							"SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4212,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4228,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4240,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char       *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4261,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4259,6 +4304,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -4332,6 +4380,7 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 
 	appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
 					  fmtId(pubinfo->dobj.name));
+
 	appendPQExpBuffer(query, " %s",
 					  fmtQualifiedDumpable(tbinfo));
 
@@ -9936,6 +9985,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17869,6 +17921,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1f08716f69..13a3b3f875 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,18 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1a5d924a23..be2f559c8b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2950,17 +2950,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6321,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND pr.prexcept = 'f'\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6321,6 +6345,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								"SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								"FROM pg_catalog.pg_class c\n"
+								"     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								"WHERE pr.prpubid = '%s'\n"
+								"  AND pr.prexcept = 't'\n"
+								"ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62ecc3cdab..83c47c48ed 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,9 +1822,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			  HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2986,7 +2990,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48205ba429..c92dd40a17 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9726fdae58..134ad05936 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* except relation */
 } PublicationTable;
 
 /*
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* An Except table */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index f8527dae02..705e2f47fc 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,27 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +204,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1656,9 +1687,14 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,9 +1711,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
@@ -1685,6 +1736,26 @@ ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA pu
  regress_publication_user | f          | t       | t       | t       | t         | f
 Tables:
     "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
 Tables from schemas:
     "public"
 
@@ -1696,13 +1767,40 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
--- Verify that publish options and publish_via_partition_root option are reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- can't add EXCEPT TABLE when the publication options are not the default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that publish option is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- can't add EXCEPT TABLE when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  Setting ALL TABLES requires publication "testpub_reset" to have default values
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that publish_via_partition_root option is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
 --------------------------+------------+---------+---------+---------+-----------+----------
- regress_publication_user | f          | f       | f       | f       | f         | t
+ regress_publication_user | f          | t       | t       | t       | t         | t
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
@@ -1716,11 +1814,12 @@ ALTER PUBLICATION testpub_reset RESET;
 -- Verify that only superuser can reset a publication
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
-ALTER PUBLICATION testpub_reset RESET; -- fail
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 868f1c51b1..5d639eea62 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,30 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1060,26 +1070,63 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- can't add EXCEPT TABLE when the publication options are not the default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- Verify that publish option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- can't add EXCEPT TABLE when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that publish options and publish_via_partition_root option are reset
+-- Verify that publish_via_partition_root option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1092,6 +1139,7 @@ SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..cd76f5bc3d
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,85 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#42vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#36)
Re: Skipping schema changes in publication

On Mon, May 16, 2022 at 2:53 PM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v5-0001.

There is some overlap with comments recently posted by Osumi-san [1].

(I also have review comments for v5-0002; will post them tomorrow)

======

1. Commit message

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to default state which includes resetting the publication
options, setting ALL TABLES option to false and dropping the relations and
schemas that are associated with the publication.

SUGGEST
"to default state" -> "to the default state"
"ALL TABLES option" -> "ALL TABLES flag"

Modified

~~~

2. doc/src/sgml/ref/alter_publication.sgml

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> option to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
</para>

"ALL TABLES option" -> "ALL TABLES flag"

Modified

~~~

3. doc/src/sgml/ref/alter_publication.sgml

+   invoking user to be a superuser.  <literal>RESET</literal> of publication
+   requires the invoking user to be a superuser. To alter the owner, you must

SUGGESTION
To <literal>RESET</literal> a publication requires the invoking user
to be a superuser.

I have combined it with the earlier sentence.

~~~

4. src/backend/commands/publicationcmds.c

@@ -53,6 +53,13 @@
#include "utils/syscache.h"
#include "utils/varlena.h"

+#define PUB_ATION_INSERT_DEFAULT true
+#define PUB_ACTION_UPDATE_DEFAULT true
+#define PUB_ACTION_DELETE_DEFAULT true
+#define PUB_ACTION_TRUNCATE_DEFAULT true
+#define PUB_VIA_ROOT_DEFAULT false
+#define PUB_ALL_TABLES_DEFAULT false

4a.
Typo: "ATION" -> "ACTION"

Modified

4b.
I think these #defines deserve a 1 line comment.
e.g.
/* CREATE PUBLICATION default values for flags and options */

Added comment

4c.
Since the "_DEFAULT" is a common part of all the names, maybe it is
tidier if it comes first.
e.g.
#define PUB_DEFAULT_ACTION_INSERT true
#define PUB_DEFAULT_ACTION_UPDATE true
#define PUB_DEFAULT_ACTION_DELETE true
#define PUB_DEFAULT_ACTION_TRUNCATE true
#define PUB_DEFAULT_VIA_ROOT false
#define PUB_DEFAULT_ALL_TABLES false

Modified

The v6 patch attached at [1]/messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com has the changes for the same.
[1]: /messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com

Regards,
Vignesh

#43vignesh C
vignesh21@gmail.com
In reply to: shiy.fnst@fujitsu.com (#40)
Re: Skipping schema changes in publication

On Wed, May 18, 2022 at 8:30 AM shiy.fnst@fujitsu.com
<shiy.fnst@fujitsu.com> wrote:

On Sat, May 14, 2022 9:33 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v5 patch has the changes for the
same. Also I have made the changes for SKIP Table based on the new
syntax, the changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Thanks for your patch. Here are some comments on v5-0001 patch.

+               Oid                     relid = lfirst_oid(lc);
+
+               prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+                                                          ObjectIdGetDatum(relid),
+                                                          ObjectIdGetDatum(pubid));
+               if (!OidIsValid(prid))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_UNDEFINED_OBJECT),
+                                        errmsg("relation \"%s\" is not part of the publication",
+                                                       RelationGetRelationName(rel))));

I think the relation in the error message should be the one whose oid is
"relid", instead of relation "rel".

Modified it

Besides, I think it might be better not to report an error in this case. If
"prid" is invalid, just ignore this relation. Because in RESET cases, we want to
drop all tables in the publications, and there is no specific table.
(If you agree with that, similarly missing_ok should be set to true when calling
PublicationDropSchemas().)

Ideally this scenario should not happen, but if it happens I felt we
should throw an error in this case.

The v6 patch attached at [1]/messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com has the changes for the same.
[1]: /messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com

Regards,
Vignesh

#44vignesh C
vignesh21@gmail.com
In reply to: osumi.takamichi@fujitsu.com (#35)
Re: Skipping schema changes in publication

On Mon, May 16, 2022 at 2:00 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Saturday, May 14, 2022 10:33 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v5 patch has the changes for the same.
Also I have made the changes for SKIP Table based on the new syntax, the
changes for the same are available in
v5-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patch.

Hi,

Several comments on v5-0002.

(1) One unnecessary space before "except_pub_obj_list" syntax definition

+ except_pub_obj_list:  ExceptPublicationObjSpec
+                                       { $$ = list_make1($1); }
+                       | except_pub_obj_list ',' ExceptPublicationObjSpec
+                                       { $$ = lappend($1, $3); }
+                       |  /*EMPTY*/                                                            { $$ = NULL; }
+       ;
+

From above part, kindly change
FROM:
" except_pub_obj_list: ExceptPublicationObjSpec"
TO:
"except_pub_obj_list: ExceptPublicationObjSpec"

Modified

(2) doc/src/sgml/ref/create_publication.sgml

(2-1)

@@ -22,7 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]]
| FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
[ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]

Here I think we need to add two more whitespaces around square brackets.
Please change
FROM:
"[ FOR ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]]"
TO:
"[ FOR ALL TABLES [ EXCEPT TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ] ]"

When I check other documentations, I see whitespaces before/after square brackets.

(2-2)
This whitespace alignment applies to alter_publication.sgml as well.

Modified

(3)

@@ -156,6 +156,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
</listitem>
</varlistentry>

+
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>
+

This EXCEPT TABLE clause is only for FOR ALL TABLES.
So, how about extracting the main message from above part and
moving it to an exising paragraph below, instead of having one independent paragraph ?

<varlistentry>
<term><literal>FOR ALL TABLES</literal></term>
<listitem>
<para>
Marks the publication as one that replicates changes for all tables in
the database, including tables created in the future.
</para>
</listitem>
</varlistentry>

Something like
"Marks the publication as one that replicates changes for all tables in
the database, including tables created in the future. EXCEPT TABLE indicates
excluded tables for the defined publication.
"

Modified

(4) One minor confirmation about the syntax

Currently, we allow one way of writing to indicate excluded tables like below.

(example) CREATE PUBLICATION mypub FOR ALL TABLES EXCEPT TABLE tab3, tab4, EXCEPT TABLE tab5;

This is because we define ExceptPublicationObjSpec with EXCEPT TABLE.
Is it OK to have a room to write duplicate "EXCEPT TABLE" clauses ?
I think there is no harm in having this,
but I'd like to confirm whether this syntax might be better to be adjusted or not.

Changed it to allow except table only once

(5) CheckAlterPublication

+
+       if (excepttable && !stmt->for_all_tables)
+               ereport(ERROR,
+                               (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+                                               NameStr(pubform->pubname)),
+                                errdetail("except table cannot be added to, dropped from, or set on NON ALL TABLES publications.")));

Could you please add a test for this ?

This code can be removed because of grammar optimization, it will not
allow tables without "ALL TABLES". Removed this code

The v6 patch attached at [1]/messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com has the changes for the same.
[1]: /messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com

Regards,
Vignesh

#45osumi.takamichi@fujitsu.com
osumi.takamichi@fujitsu.com
In reply to: vignesh C (#41)
RE: Skipping schema changes in publication

On Thursday, May 19, 2022 2:45 AM vignesh C <vignesh21@gmail.com> wrote:

On Mon, May 16, 2022 at 8:32 AM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

(3) src/test/regress/expected/publication.out

+-- Verify that only superuser can reset a publication ALTER
+PUBLICATION testpub_reset OWNER TO regress_publication_user2; SET
+ROLE regress_publication_user2; ALTER PUBLICATION testpub_reset
+RESET; -- fail

We have "-- fail" for one case in this patch.
On the other hand, isn't better to add "-- ok" (or "-- success") for
other successful statements, when we consider the entire tests
description consistency ?

We generally do not mention success comments for all the success cases as
that might be an overkill. I felt it is better to keep it as it is.
Thoughts?

Thank you for updating the patches !

In terms of this point,
I meant to say we add "-- ok" for each successful
"ALTER PUBLICATION testpub_reset RESET;" statement.
That means, we'll have just three places to add "--ok"
and I thought this was not an overkill.

*But*, I'm also OK with your idea.
Please don't change the comments
and keep them as it is like v6.

Best Regards,
Takamichi Osumi

#46vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#37)
Re: Skipping schema changes in publication

On Tue, May 17, 2022 at 7:35 AM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v5-0002.

There may be an overlap with comments recently posted by Osumi-san [1].

(I also have review comments for v5-0002; will post them tomorrow)

======

1. General

Is it really necessary to have to say "EXCEPT TABLE" instead of just
"EXCEPT". It seems unnecessarily verbose and redundant when you write
"FOR ALL TABLES EXCEPT TABLE...".

If you want to keep this TABLE keyword (maybe you have plans for other
kinds of except?) then IMO perhaps at least it can be the optional
default except type. e.g. EXCEPT [TABLE].

I have made TABLE optional.

~~~

2. General

(I was unsure whether to even mention this one).

I understand the "EXCEPT" is chosen as the user-facing syntax, but it
still seems strange when reading the patch to see attribute members
and column names called 'except'. I think the problem is that "except"
is not a verb, so saying except=t/f just does not make much sense.
Sometimes I feel that for all the internal usage
(code/comments/catalog) using "skip" and "skip-list" etc would be a
much better choice of names. OTOH I can see that having consistency
with the outside syntax might also be good. Anyway, please consider -
maybe other people feel the same?

Earlier we had discussed whether to use SKIP, but felt SKIP was not
appropriate and planned to use except as in [1]/messages/by-id/a2004f08-eb2f-b124-115c-f8f18667e585@enterprisedb.com. Let's use except
unless we find a better alternative.

~~~

3. General

The ONLY keyword seems supported by the syntax for tables of the
except-list (more on this in later comments) but:
a) I am not sure if the patch code is accounting for that, and

I have kept the behavior similar to FOR TABLE

b) There are no test cases using ONLY.

Added tests for the same

~~~

4. Commit message

A new option "EXCEPT TABLE" in Create/Alter Publication allows
one or more tables to be excluded, publisher will exclude sending the data
of the excluded tables to the subscriber.

SUGGESTION
A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

Modified

~~

5. Commit message

The new syntax allows specifying exclude relations while creating a publication
or exclude relations in alter publication. For example:

SUGGESTION
The new syntax allows specifying excluded relations when creating or
altering a publication. For example:

Modified

~~~

6. Commit message

A new column prexcept is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude publishing through the publication.

SUGGESTION
A new column "prexcept" is added to table "pg_publication_rel", to
maintain the relations that the user wants to exclude from the
publications.

Modified

~~~

7. Commit message

Modified the output plugin (pgoutput) to exclude publishing the changes of the
excluded tables.

I did not feel it was necessary to say this. It is already said above
that the data is not sent, so that seems enough.

Modified

~~~

8. Commit message

Updates pg_dump to identify and dump the excluded tables of the publications.
Updates the \d family of commands to display excluded tables of the
publications and \dRp+ variant will now display associated except tables if any.

SUGGESTION
pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands to display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Modified

~~~

9. doc/src/sgml/catalogs.sgml

@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration
count&gt;</replaceable>:<replaceable>&l
if there is no publication qualifying condition.</para></entry>
</row>

+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the table must be excluded
+      </para></entry>
+     </row>

Other descriptions on this page refer to "relation" instead of
"table". Probably this should do the same to be consistent.

Modified

~~~

10. doc/src/sgml/logical-replication.sgml

@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication
origin "pg_16395" during "INSER
<para>
To add tables to a publication, the user must have ownership rights on the
table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
</para>

It seems like a valid change but how is this related to this EXCEPT
patch. Maybe this fix should be patched separately?

Earlier we were not allowed to add ALL TABLES, while altering
publication. This is mentioned in this patch as we suport:
ALTER PUBLICATION pubname ADD ALL TABLES syntax.

~~~

11. doc/src/sgml/ref/alter_publication.sgml

@@ -22,6 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD <replaceable class="parameter">publication_object</replaceable> [,
...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [EXCEPT TABLE [ ONLY ] <replaceable
class="parameter">table_name</replaceable> [ * ] [, ... ]]

The [ONLY] looks misplaced when the syntax is described like this. For
example, in practice it is possible to write "EXCEPT TABLE ONLY t1,
ONLY t2, t3, ONLY t4" but it doesn't seem that way by looking at these
PG DOCS.

IMO would be better described like this:

[ FOR ALL TABLES [ EXCEPT TABLE exception_object [,...] ]]

where exception_object is:

[ ONLY ] table_name [ * ]

Modified

~~~

12. doc/src/sgml/ref/alter_publication.sgml

@@ -82,8 +83,8 @@ ALTER PUBLICATION <replaceable
class="parameter">name</replaceable> RESET

<para>
You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
+   Adding a table or excluding a table to a publication additionally requires
+   owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal> and

SUGGESTION
Adding a table to or excluding a table from a publication additionally
requires owning that table.

Modified

~~~

13. doc/src/sgml/ref/alter_publication.sgml

@@ -213,6 +214,14 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
</programlisting>
</para>

+  <para>
+   Alter publication <structname>production_publication</structname> that
+   publishes all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>

"that publishes" -> "to publish"

Modified

~~~

14. doc/src/sgml/ref/create_publication.sgml

(Same comment about the ONLY syntax as #11)

Modified

~~~

15. doc/src/sgml/ref/create_publication.sgml

+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      Marks the publication as one that excludes replicating changes for the
+      specified tables.
+     </para>
+
+     <para>
+      <literal>EXCEPT TABLE</literal> can be specified only for
+      <literal>FOR ALL TABLES</literal> publication. It is not supported for
+      <literal>FOR ALL TABLES IN SCHEMA </literal> publication and
+      <literal>FOR TABLE</literal> publication.
+     </para>
+    </listitem>
+   </varlistentry>

IMO you can remove all that "It is not supported for..." sentence. You
don't need to spell that out again when it is already clear from the
syntax.

Modified

~~~

16. doc/src/sgml/ref/psql-ref.sgml

@@ -1868,8 +1868,9 @@ testdb=&gt;
If <replaceable class="parameter">pattern</replaceable> is
specified, only those publications whose names match the pattern are
listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication
are shown as
+        well.
</para>

Perhaps this is OK just as-is, but OTOH I felt that the change was
almost unnecessary because saying it displays "the tables" kind of
implies it would also have to account for the "excluded tables" too.

I mentioned it that way so that it is clearer and to avoid confusions
to be pointed out by other members later. I felt let's keep it this
way.

~~~

17. src/backend/catalog/pg_publication.c - GetTopMostAncestorInPublication

@@ -302,8 +303,9 @@ GetTopMostAncestorInPublication(Oid puboid, List
*ancestors, int *ancestor_level
foreach(lc, ancestors)
{
Oid ancestor = lfirst_oid(lc);
- List    *apubids = GetRelationPublications(ancestor);
+ List    *apubids = GetRelationPublications(ancestor, false);
List    *aschemaPubids = NIL;
+ List    *aexceptpubids = NIL;

17a.
I think the var "aschemaPubids" and "aexceptpubids" are only used in
the 'else' block so it seems better they can be declared and freed in
that block too instead of always.

Modified

17b.
Also, the camel-case of those variables is inconsistent so may fix
that at the same time.

Modified

~~~

18. src/backend/catalog/pg_publication.c - GetRelationPublications

@@ -666,7 +673,7 @@ publication_add_schema(Oid pubid, Oid schemaid,
bool if_not_exists)

/* Gets list of publication oids for a relation */
List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool bexcept)

18a.
I felt that "except_flag" is a better name than "bexcept" for this param.

Modified

18b.
The function comment should be updated to say only relations matching
this except_flag are returned in the list.

Modified

~~~

19. src/backend/catalog/pg_publication.c - GetAllTablesPublicationRelations

@@ -787,6 +795,15 @@ GetAllTablesPublicationRelations(bool pubviaroot)
HeapTuple tuple;
List *result = NIL;

+ /*
+ * pg_publication_rel and pg_publication_namespace will only have excluded
+ * tables in case of all tables publication, no need to pass except flag
+ * to get the relations.
+ */
+ List    *exceptpubtablelist;
+
+ exceptpubtablelist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
+

19a.
I wasn't very sure of the meaning/intent of the comment, but IIUC it
seems to be explaining why it is not necessary to use an "except_flag"
parameter in this code. Is it necessary/helpful to explain parameters
that do NOT exist?

I have removed it

19b.
The var name "exceptpubtablelist" seems a bit overkill. (e.g.
"excepttablelist" or "exceptlist" etc... are shorter but seem equally
informative).

Modified

~~~

20. src/backend/commands/publicationcmds.c - CreatePublication

@@ -843,54 +849,52 @@ CreatePublication(ParseState *pstate,
CreatePublicationStmt *stmt)
/* Make the changes visible. */
CommandCounterIncrement();

- /* Associate objects with the publication. */
- if (stmt->for_all_tables)
- {
- /* Invalidate relcache so that publication info is rebuilt. */
- CacheInvalidateRelcacheAll();
- }
- else
- {
- ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-    &schemaidlist);
+ ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+ &schemaidlist);
- /* FOR ALL TABLES IN SCHEMA requires superuser */
- if (list_length(schemaidlist) > 0 && !superuser())
- ereport(ERROR,
- errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+ /* FOR ALL TABLES IN SCHEMA requires superuser */
+ if (list_length(schemaidlist) > 0 && !superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
- if (list_length(relations) > 0)
- {
- List    *rels;
+ if (list_length(relations) > 0)
+ {
+ List    *rels;
- rels = OpenTableList(relations);
- CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-   PUBLICATIONOBJ_TABLE);
+ rels = OpenTableList(relations);
+ CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+ PUBLICATIONOBJ_TABLE);
- TransformPubWhereClauses(rels, pstate->p_sourcetext,
- publish_via_partition_root);
+ TransformPubWhereClauses(rels, pstate->p_sourcetext,
+ publish_via_partition_root);
- CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-    publish_via_partition_root);
+ CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+ publish_via_partition_root);
- PublicationAddTables(puboid, rels, true, NULL);
- CloseTableList(rels);
- }
+ PublicationAddTables(puboid, rels, true, NULL);
+ CloseTableList(rels);
+ }
- if (list_length(schemaidlist) > 0)
- {
- /*
- * Schema lock is held until the publication is created to prevent
- * concurrent schema deletion.
- */
- LockSchemaList(schemaidlist);
- PublicationAddSchemas(puboid, schemaidlist, true, NULL);
- }
+ if (list_length(schemaidlist) > 0)
+ {
+ /*
+ * Schema lock is held until the publication is created to prevent
+ * concurrent schema deletion.
+ */
+ LockSchemaList(schemaidlist);
+ PublicationAddSchemas(puboid, schemaidlist, true, NULL);
}

table_close(rel, RowExclusiveLock);

+ /* Associate objects with the publication. */
+ if (stmt->for_all_tables)
+ {
+ /* Invalidate relcache so that publication info is rebuilt. */
+ CacheInvalidateRelcacheAll();
+ }
+

This function is refactored a lot to not use "if/else" as it did
before. But AFAIK (maybe I misunderstood) this refactor doesn't seem
to actually have anything to do with the EXCEPT patch. If it really is
unrelated maybe it should not be part of this patch.

Earlier tables cannot be specified with all tables, now except tables
can be specified with all tables, except tables should be added to
pg_publication_rel, to handle it the code changes are required.

~~~

21. src/backend/commands/publicationcmds.c - CheckPublicationDefValues

+ if (pubform->puballtables)
+ return false;
+
+ if (!pubform->pubinsert || !pubform->pubupdate || !pubform->pubdelete ||
+ !pubform->pubtruncate || pubform->pubviaroot)
+ return false;

Now you have all the #define for the PUB_DEFAULT_XXX values, perhaps
this function should be using them instead of the hardcoded
assumptions what the default values are.

e.g.

if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES) return false;
if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT) return false;
...
etc.

Modified

~~~

22. src/backend/commands/publicationcmds.c - CheckAlterPublication

@@ -1442,6 +1516,19 @@ CheckAlterPublication(AlterPublicationStmt
*stmt, HeapTuple tup,
List *tables, List *schemaidlist)
{
Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+ ListCell   *lc;
+ bool nonexcepttable = false;
+ bool excepttable = false;
+
+ foreach(lc, tables)
+ {
+ PublicationTable *pub_table = lfirst_node(PublicationTable, lc);
+
+ if (!pub_table->except)
+ nonexcepttable = true;
+ else
+ excepttable = true;
+ }

22a.
The names are very confusing. e.g. "nonexcepttable" is like a double-negative.

SUGGEST:
bool has_tables = false;
bool has_except_tables = false;

22b.
Reverse the "if" condition to be positive instead of negative (remove !)
e.g.
if (pub_table->except)
has_except_table = true;
else
has_table = true;

This code can be removed because of grammar optimization, it will not
allow except table without "ALL TABLES". Removed these changes.

~~~

23. src/backend/commands/publicationcmds.c - CheckAlterPublication

@@ -1461,12 +1548,19 @@ CheckAlterPublication(AlterPublicationStmt
*stmt, HeapTuple tup,
errdetail("Tables from schema cannot be added to, dropped from, or
set on FOR ALL TABLES publications.")));

/* Check that user is allowed to manipulate the publication tables. */
- if (tables && pubform->puballtables)
+ if (nonexcepttable && tables && pubform->puballtables)
ereport(ERROR,

Seems no reason for "tables" to be in the condition since
"nonexcepttable" can't be true if "tables" is NIL.

This code can be removed because of grammar optimization, it will not
allow except table without "ALL TABLES". Removed these changes.

~~~

24. src/backend/commands/publicationcmds.c - CheckAlterPublication

+
+ if (excepttable && !stmt->for_all_tables)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("publication \"%s\" is not defined as FOR ALL TABLES",
+ NameStr(pubform->pubname)),
+ errdetail("except table cannot be added to, dropped from, or set on
NON ALL TABLES publications.")));

The errdetail message seems over-complex.

SUGGESTION
"EXCEPT TABLE clause is only allowed for FOR ALL TABLES publications."

This code can be removed because of grammar optimization, it will not
allow except table without "ALL TABLES". Removed this code

~~~

25. src/backend/commands/publicationcmds.c - AlterPublication

@@ -1500,6 +1594,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Either the publication has tables/schemas associated or
does not have default publication options or ALL TABLES option is
set."));

The errhint message seems over-complex.

SUGGESTION
"Use ALTER PUBLICATION ... RESET"

Modified

~~~

26. src/bin/pg_dump/pg_dump.c - dumpPublication

@@ -3980,8 +3982,34 @@ dumpPublication(Archive *fout, const
PublicationInfo *pubinfo)
qpubname);

if (pubinfo->puballtables)
+ {
+ SimplePtrListCell *cell;
+ bool first = true;
appendPQExpBufferStr(query, " FOR ALL TABLES");
+ /* Include exception tables if the publication has except tables */
+ for (cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ PublicationInfo *relpubinfo = pubrinfo->publication;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == relpubinfo)
+ {
+ tbinfo = pubrinfo->pubtable;
+
+ if (first)
+ {
+ appendPQExpBufferStr(query, " EXCEPT TABLE ONLY");
+ first = false;
+ }
+ else
+ appendPQExpBufferStr(query, ", ");
+ appendPQExpBuffer(query, " %s", fmtQualifiedDumpable(tbinfo));
+ }
+ }
+ }
+

IIUC this usage of ONLY looks incorrect.

26a.
Firstly, if you want to hardwire ONLY then shouldn't it apply to every
of the except-list table, not just the first one? e.g. "EXCEPT TABLE
ONLY t1, ONLY t2, ONLY t3..."

Modified, included ONLY for all the tables

26b.
Secondly, is it even correct to unconditionally hardwire the ONLY? How
do you know that is how the user wanted it?

The table ONLY selection is handled appropriately while creating
publication and stored in pg_publication_rel. When we dump all the
parent and child table will be included specifying ONLY will handle
both scenarios with and without ONLY. This is the same behavior as in
FOR TABLE publication

~~~

27. src/bin/pg_dump/pg_dump.c

@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids
= {NULL, NULL};
static SimpleStringList extension_include_patterns = {NULL, NULL};
static SimpleOidList extension_include_oids = {NULL, NULL};

+static SimplePtrList exceptinfo = {NULL, NULL};
+

Probably I just did not understand how this logic works, but how does
this static work properly if there are multiple publications and 2
different EXCEPT lists? E.g. where is it clearing the "exceptinfo" so
that multiple EXCEPT TABLE lists don't become muddled?

Currently exceptinfo holds all the exception tables and the
corresponding publications. When we dump the publication it will
select the appropriate exception tables that correspond to the
publication and dump the exception tables associated for this
publication. Since this is a special syntax "CREATE PUBLICATION FOR
ALL TABLES EXCEPT TABLE tb1 .." all the except tables should be
specified in a single statement unlike the other publication objects.

~~~

28. src/bin/pg_dump/pg_dump.c - dumpPublicationTable

@@ -4330,8 +4378,11 @@ dumpPublicationTable(Archive *fout, const
PublicationRelInfo *pubrinfo)

query = createPQExpBuffer();

- appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
+ appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD ",
fmtId(pubinfo->dobj.name));
+
+ appendPQExpBufferStr(query, "TABLE ONLY");
+

That code refactor does not seem necessary for this patch.

Modified

~~~

29. src/bin/pg_dump/pg_dump_sort.c

@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
PRIO_FK_CONSTRAINT,
PRIO_POLICY,
PRIO_PUBLICATION,
+ PRIO_PUBLICATION_EXCEPT_REL,
PRIO_PUBLICATION_REL,
PRIO_PUBLICATION_TABLE_IN_SCHEMA,
PRIO_SUBSCRIPTION,

I'm not sure how this enum is used (so perhaps this makes no
difference) but judging by the enum comment why did you put the sort
priority order PRIO_PUBLICATION_EXCEPT_REL before
PRIO_PUBLICATION_REL. Wouldn’t it make more sense the other way
around?

This order does not matter, since the new syntax is like "CREATE
PUBLICATION.. FOR ALL TABLES EXCEPT TABLE ....", all the except tables
need to be accumulated and handled during dump publication. This code
changes take care of accumulating the exception table which will be
used later by dump publication

~~~

30. src/bin/psql/describe.c

@@ -2950,17 +2950,34 @@ describeOneTableDetails(const char *schemaname,
"          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
"        ELSE NULL END) "
"FROM pg_catalog.pg_publication p\n"
-   "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-   "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-   "WHERE pr.prrelid = '%s'\n"
-   "UNION\n"
+   " JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+   " JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+   "WHERE pr.prrelid = '%s'",
+   oid, oid, oid);

I feel that trailing "\n" ("WHERE pr.prrelid = '%s'\n") should not
have been removed.

Modified

~~~

31. src/bin/psql/describe.c

+ /* FIXME: 150000 should be changed to 160000 later for PG16. */
+ if (pset.sversion >= 150000)
+ appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+ appendPQExpBuffer(&buf, "UNION\n"

The "UNION\n" param might be better wrapped onto the next line like it
used to be.

Modified

~~~

32. src/bin/psql/describe.c

+ /* FIXME: 150000 should be changed to 160000 later for PG16. */
+ if (pset.sversion >= 150000)
+ appendPQExpBuffer(&buf,
+   " AND NOT EXISTS (SELECT 1\n"
+   " FROM pg_catalog.pg_publication_rel pr\n"
+   " JOIN pg_catalog.pg_class pc\n"
+   "   ON pr.prrelid = pc.oid\n"
+   " WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+   oid);

The whitespace indents in the SQL seem excessive here.

Modified

~~~

33. src/bin/psql/describe.c - describePublications

@@ -6322,6 +6344,22 @@ describePublications(const char *pattern)
}
}

+ /* FIXME: 150000 should be changed to 160000 later for PG16. */
+ if (pset.sversion >= 150000)
+ {
+ /* Get the excluded tables for the specified publication */
+ printfPQExpBuffer(&buf,
+   "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+   "FROM pg_catalog.pg_class c\n"
+   "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+   "WHERE pr.prpubid = '%s'\n"
+   "  AND pr.prexcept = 't'\n"
+   "ORDER BY 1", pubid);
+ if (!addFooterToPublicationDesc(&buf, "Except tables:",
+ true, &cont))
+ goto error_return;
+ }
+

I think this code is misplaced. Shouldn't it be if/else and be above
the other 150000 check, otherwise when you change this to PG16 it may
not work as expected?

I moved this to else. I felt this is applicable only for all tables
publication. Just keeping in else is fine.

~~~

34. src/bin/psql/describe.c - describePublications

+ if (!addFooterToPublicationDesc(&buf, "Except tables:",
+ true, &cont))
+ goto error_return;
+ }

Should this be using the _T() macros same as the other prompts for translation?

Modified

~~~

35. src/include/catalog/pg_publication.h

I thought the param "bexpect" should be "except_flag".

(same comment as #18a)

Modified

~~~

36. src/include/catalog/pg_publication_rel.h

@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
Oid oid; /* oid */
Oid prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
Oid prrelid BKI_LOOKUP(pg_class); /* Oid of the relation */
+ bool prexcept BKI_DEFAULT(f); /* except the relation */

SUGGEST (comment)
/* skip the relation */

Changed it to exclude the relation

~~~

37. src/include/commands/publicationcmds.h

@@ -32,8 +32,8 @@ extern ObjectAddress AlterPublicationOwner(const
char *name, Oid newOwnerId);
extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
extern void InvalidatePublicationRels(List *relids);
extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-    List *ancestors, bool pubviaroot);
+    List *ancestors, bool pubviaroot, bool alltables);
extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
- List *ancestors, bool pubviaroot);
+ List *ancestors, bool pubviaroot, bool alltables);

Elsewhere in this patch, a similarly added param is called
"puballtables" (not "alltables"). Please check all places and use a
consistent param name for all of them.

Modified

~~~

38. src/test/regress/sql/publication.sql

There don't seem to be any tests for more than one EXCEPT TABLE (e.g.
no list tests?)

Modified

~~~

38. src/test/regress/sql/publication.sql

Maybe adjust all the below comments (a-d) to say "EXCEPT TABLES"
intead of "except tables"

38a.
+-- can't add except table to 'FOR ALL TABLES' publication

38b.
+-- can't add except table to 'FOR TABLE' publication

38c.
+-- can't add except table to 'FOR ALL TABLES IN SCHEMA' publication

38d.
+-- can't add except table when publish_via_partition_root option does not
+-- have default value
38e.
+-- can't add except table when the publication options does not have default
+-- values

SUGGESTION
can't add EXCEPT TABLE when the publication options are not the default values

Modified

~~~

39. .../t/032_rep_changes_except_table.pl

39a.
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+ "SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check tablesync is excluded for excluded tables');

Maybe the "is" message should say "check there is no initial data
copied for the excluded table"

Modified

~~~

40 .../t/032_rep_changes_except_table.pl

+# Insert some data into few tables and verify that inserted data is not
+# replicated
+$node_publisher->safe_psql('postgres',
+ "INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");

The comment is not quite correct. You are inserting into only one
table here - not "few tables".

Modified

~~~

41. .../t/032_rep_changes_except_table.pl

+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the new table data.

"new table data" -> "changed data for this table"

Modified

Thanks for the comments, the v6 patch attached at [2]/messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com has the changes
for the same.
[1]: /messages/by-id/a2004f08-eb2f-b124-115c-f8f18667e585@enterprisedb.com
[2]: /messages/by-id/CALDaNm0iZZDB300Dez_97S8G6_RW5QpQ8ef6X3wq8tyK-8wnXQ@mail.gmail.com

Regards,
Vignesh

#47Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#41)
Re: Skipping schema changes in publication

Below are my review comments for v6-0001.

======

1. General.

The patch failed 'publication' tests in the make check phase.

Please add this work to the commit-fest so that the 'cfbot' can report
such errors sooner.

~~~

2. src/backend/commands/publicationcmds.c - AlterPublicationReset

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and
publication schemas.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+ Relation rel, HeapTuple tup)

SUGGESTION (Make the comment similar to the sgml text instead of
repeating "publication" 4x !)
/*
* Reset the publication options, set the ALL TABLES flag to false, and
* drop all relations and schemas that are associated with the publication.
*/

~~~

3. src/test/regress/expected/publication.out

make check failed. The diff is below:

@@ -1716,7 +1716,7 @@
 -- Verify that only superuser can reset a publication
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
-ALTER PUBLICATION testpub_reset RESET; -- fail
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;

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

#48Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#41)
Re: Skipping schema changes in publication

FYI, although the v6-0002 patch applied cleanly, I found that the SGML
was malformed and so the pg docs build fails.

~~~
e.g.

[postgres@CentOS7-x64 sgml]$ make STYLE=website html
{ \
echo "<!ENTITY version \"15beta1\">"; \
echo "<!ENTITY majorversion \"15\">"; \
} > version.sgml
'/usr/bin/perl' ./mk_feature_tables.pl YES
../../../src/backend/catalog/sql_feature_packages.txt
../../../src/backend/catalog/sql_features.txt >
features-supported.sgml
'/usr/bin/perl' ./mk_feature_tables.pl NO
../../../src/backend/catalog/sql_feature_packages.txt
../../../src/backend/catalog/sql_features.txt >
features-unsupported.sgml
'/usr/bin/perl' ./generate-errcodes-table.pl
../../../src/backend/utils/errcodes.txt > errcodes-table.sgml
'/usr/bin/perl' ./generate-keywords-table.pl . > keywords-table.sgml
/usr/bin/xmllint --path . --noout --valid postgres.sgml
ref/create_publication.sgml:171: parser error : Opening and ending tag
mismatch: varlistentry line 166 and listitem
</listitem>
^
ref/create_publication.sgml:172: parser error : Opening and ending tag
mismatch: variablelist line 60 and varlistentry
</varlistentry>
^
ref/create_publication.sgml:226: parser error : Opening and ending tag
mismatch: refsect1 line 57 and variablelist
</variablelist>
^
...

I will work around it locally, but for future patches please check the
SGML builds ok before posting.

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

#49Peter Smith
smithpb2250@gmail.com
In reply to: Peter Smith (#48)
Re: Skipping schema changes in publication

On Fri, May 20, 2022 at 10:19 AM Peter Smith <smithpb2250@gmail.com> wrote:

FYI, although the v6-0002 patch applied cleanly, I found that the SGML
was malformed and so the pg docs build fails.

~~~
e.g.

[postgres@CentOS7-x64 sgml]$ make STYLE=website html
{ \
echo "<!ENTITY version \"15beta1\">"; \
echo "<!ENTITY majorversion \"15\">"; \
} > version.sgml
'/usr/bin/perl' ./mk_feature_tables.pl YES
../../../src/backend/catalog/sql_feature_packages.txt
../../../src/backend/catalog/sql_features.txt >
features-supported.sgml
'/usr/bin/perl' ./mk_feature_tables.pl NO
../../../src/backend/catalog/sql_feature_packages.txt
../../../src/backend/catalog/sql_features.txt >
features-unsupported.sgml
'/usr/bin/perl' ./generate-errcodes-table.pl
../../../src/backend/utils/errcodes.txt > errcodes-table.sgml
'/usr/bin/perl' ./generate-keywords-table.pl . > keywords-table.sgml
/usr/bin/xmllint --path . --noout --valid postgres.sgml
ref/create_publication.sgml:171: parser error : Opening and ending tag
mismatch: varlistentry line 166 and listitem
</listitem>
^
ref/create_publication.sgml:172: parser error : Opening and ending tag
mismatch: variablelist line 60 and varlistentry
</varlistentry>
^
ref/create_publication.sgml:226: parser error : Opening and ending tag
mismatch: refsect1 line 57 and variablelist
</variablelist>
^
...

I will work around it locally, but for future patches please check the
SGML builds ok before posting.

FYI, I rewrote the bad SGML fragment like this:

<varlistentry>
<term><literal>EXCEPT TABLE</literal></term>
<listitem>
<para>
This clause specifies a list of tables to exclude from the publication. It
can only be used with <literal>FOR ALL TABLES</literal>.
</para>
</listitem>
</varlistentry>

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

#50Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#41)
Re: Skipping schema changes in publication

Below are my review comments for v6-0002.

======

1. Commit message.
The psql \d family of commands to display excluded tables.

SUGGESTION
The psql \d family of commands can now display excluded tables.

~~~

2. doc/src/sgml/ref/alter_publication.sgml

@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD <replaceable class="parameter">publication_object</replaceable> [,
...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]

The "exception_object" font is wrong. Should look the same as
"publication_object"

~~~

3. doc/src/sgml/ref/alter_publication.sgml - Examples

@@ -214,6 +220,14 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
</programlisting>
</para>

+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT TABLE
users, departments;
+</programlisting></para>

Consider using "EXCEPT" instead of "EXCEPT TABLE" because that will
show TABLE keyword is optional.

~~~

4. doc/src/sgml/ref/create_publication.sgml

An SGML tag error caused building the docs to fail. My fix was
previously reported [1]/messages/by-id/CAHut+PtZDfBJ1d=3kSexgM5m+P_ok8sdsJXKimsXycaMyqXsNA@mail.gmail.com.

~~~

5. doc/src/sgml/ref/create_publication.sgml

@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]

The "exception_object" font is wrong. Should look the same as
"publication_object"

~~~

6. doc/src/sgml/ref/create_publication.sgml - Examples

@@ -351,6 +366,15 @@ CREATE PUBLICATION production_publication FOR
TABLE users, departments, ALL TABL
CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
</programlisting></para>

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+

6a.
Typo: "FOR ALL TABLE" -> "FOR ALL TABLES"

6b.
Consider using "EXCEPT" instead of "EXCEPT TABLE" because that will
show TABLE keyword is optional.

~~~

7. src/backend/catalog/pg_publication.c - GetTopMostAncestorInPublication

@@ -316,18 +316,25 @@ GetTopMostAncestorInPublication(Oid puboid, List
*ancestors, int *ancestor_level
  }
  else
  {
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
+ List    *aschemapubids = NIL;
+ List    *aexceptpubids = NIL;
+
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+ aexceptpubids = GetRelationPublications(ancestor, true);
+ if (list_member_oid(aschemapubids, puboid) ||
+ (puballtables && !list_member_oid(aexceptpubids, puboid)))
  {

You could re-write this as multiple conditions instead of one. That
could avoid always assigning the 'aexceptpubids', so it might be a
more efficient way to write this logic.

~~~

8. src/backend/catalog/pg_publication.c - CheckPublicationDefValues

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * Publication is having default options
+ *  Publication is not associated with relations
+ *  Publication is not associated with schemas
+ *  Publication is not set with "FOR ALL TABLES"
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

8a.
Remove the tab. Replace with spaces.

8b.
It might be better if this comment order is the same as the logic order.
e.g.

* Check the following:
* Publication is not set with "FOR ALL TABLES"
* Publication is having default options
* Publication is not associated with schemas
* Publication is not associated with relations

~~~

9. src/backend/catalog/pg_publication.c - AlterPublicationSetAllTables

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and
publication schemas.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)

The function comment and the function name do not seem to match here;
something looks like a cut/paste error ??

~~~

10. src/backend/catalog/pg_publication.c - AlterPublicationSetAllTables

+ /* set all tables option */
+ values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+ replaces[Anum_pg_publication_puballtables - 1] = true;

SUGGEST (comment)
/* set all ALL TABLES flag */

~~~

11. src/backend/catalog/pg_publication.c - AlterPublication

@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg should start with a lowercase letter.

~~~

12. src/backend/catalog/pg_publication.c - AlterPublication

@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

Example test:

postgres=# create table t1(a int);
CREATE TABLE
postgres=# create publication p1 for table t1;
CREATE PUBLICATION
postgres=# alter publication p1 add all tables except t1;
2022-05-20 14:34:49.301 AEST [21802] ERROR: Setting ALL TABLES
requires publication "p1" to have default values
2022-05-20 14:34:49.301 AEST [21802] HINT: Use ALTER PUBLICATION ...
RESET to reset the publication
2022-05-20 14:34:49.301 AEST [21802] STATEMENT: alter publication p1
add all tables except t1;
ERROR: Setting ALL TABLES requires publication "p1" to have default values
HINT: Use ALTER PUBLICATION ... RESET to reset the publication
postgres=# alter publication p1 set all tables except t1;

That error message does not quite match what the user was doing.
Firstly, they were adding the ALL TABLES, not setting it. Secondly,
all the values of the publication were already defaults (only there
was an existing table t1 in the publication). Maybe some minor changes
to the message wording can be a better reflect what the user is doing
here.

~~~

13. src/backend/parser/gram.y

@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE
aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT TABLE table [, ...]]
[WITH options]

Comment should show the "TABLE" keyword is optional

~~~

14. src/bin/pg_dump/pg_dump.c - dumpPublicationTable

@@ -4332,6 +4380,7 @@ dumpPublicationTable(Archive *fout, const
PublicationRelInfo *pubrinfo)

appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
fmtId(pubinfo->dobj.name));
+
appendPQExpBuffer(query, " %s",
fmtQualifiedDumpable(tbinfo));

This additional whitespace seems unrelated to this patch

~~~

15. src/include/nodes/parsenodes.h

15a.
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
RangeVar *relation; /* relation to be published */
Node *whereClause; /* qualifications */
List *columns; /* List of columns in a publication table */
+ bool except; /* except relation */
} PublicationTable;

Maybe the comment should be more like similar ones:
/* exclude the relation */

15b.
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
  PUBLICATIONOBJ_TABLE, /* A table */
+ PUBLICATIONOBJ_EXCEPT_TABLE, /* An Except table */
  PUBLICATIONOBJ_TABLES_IN_SCHEMA, /* All tables in schema */
  PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA, /* All tables in first element of

Maybe the comment should be more like:
/* A table to be excluded */

~~~

16. src/test/regress/sql/publication.sql

I did not see any test cases using EXCEPT when the optional TABLE
keyword is omitted.

------
[1]: /messages/by-id/CAHut+PtZDfBJ1d=3kSexgM5m+P_ok8sdsJXKimsXycaMyqXsNA@mail.gmail.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#51vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#47)
2 attachment(s)
Re: Skipping schema changes in publication

On Thu, May 19, 2022 at 1:49 PM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v6-0001.

======

1. General.

The patch failed 'publication' tests in the make check phase.

Please add this work to the commit-fest so that the 'cfbot' can report
such errors sooner.

Added commitfest entry

~~~

2. src/backend/commands/publicationcmds.c - AlterPublicationReset

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and
publication schemas.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+ Relation rel, HeapTuple tup)

SUGGESTION (Make the comment similar to the sgml text instead of
repeating "publication" 4x !)
/*
* Reset the publication options, set the ALL TABLES flag to false, and
* drop all relations and schemas that are associated with the publication.
*/

Modified

~~~

3. src/test/regress/expected/publication.out

make check failed. The diff is below:

@@ -1716,7 +1716,7 @@
-- Verify that only superuser can reset a publication
ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
SET ROLE regress_publication_user2;
-ALTER PUBLICATION testpub_reset RESET; -- fail
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
ERROR:  must be superuser to RESET publication
SET ROLE regress_publication_user;
DROP PUBLICATION testpub_reset;

It passed for me locally because the change was present in the 002
patch. I have moved the change to 001.

The attached v7 patch has the changes for the same.
[1]: https://commitfest.postgresql.org/38/3646/

Regards,
Vignesh

Attachments:

v7-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 1855e00f2d6cc19427c55eec2f1e40ecc8f8c1cc Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v7 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
options, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  38 ++++++--
 src/backend/commands/publicationcmds.c    | 100 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out |  69 +++++++++++++++
 src/test/regress/sql/publication.sql      |  37 ++++++++
 7 files changed, 242 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..47bd15f1fa 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -207,6 +221,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 8e645741e4..9ed8cdedbc 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and options */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,86 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication options */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1504,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 989db0dbec..d7e13666a2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10558,6 +10558,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10604,6 +10606,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 55af9eb04e..62ecc3cdab 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1819,7 +1819,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 73f635b455..9726fdae58 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4035,7 +4035,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 274b37dfe5..799a3f15f5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,75 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..868f1c51b1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,43 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

v7-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v7-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From d150b7acf0ee86dc30e72266231e3dbaa2b1b51b Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 17 May 2022 11:50:00 +0530
Subject: [PATCH v7 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   5 +-
 doc/src/sgml/ref/alter_publication.sgml       |  18 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  71 ++++---
 src/backend/commands/publicationcmds.c        | 175 +++++++++++++-----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  39 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  57 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  12 ++
 src/bin/psql/describe.c                       |  62 ++++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     | 125 ++++++++++++-
 src/test/regress/sql/publication.sql          |  65 ++++++-
 .../t/032_rep_changes_except_table.pl         |  85 +++++++++
 24 files changed, 698 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d96c72e531..985eda16a7 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..d7d6ba0529 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   <para>
    To add tables to a publication, the user must have ownership rights on the
    table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 47bd15f1fa..82a4ea4ec1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -214,6 +220,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..92916bf72c 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +367,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..3889796b3f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 8c7fca62de..be8282a3c3 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,43 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		if (!list_member_oid(apubids, puboid))
 		{
-			topmost_relid = ancestor;
-
-			if (ancestor_level)
-				*ancestor_level = level;
-		}
-		else
-		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			/* check if member of schema publications */
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+			if (!list_member_oid(aschemapubids, puboid))
 			{
-				topmost_relid = ancestor;
-
-				if (ancestor_level)
-					*ancestor_level = level;
+				/*
+				 * If the publication is all tables publication and the table
+				 * is not part of exception tables.
+				 */
+				if (puballtables)
+				{
+					aexceptpubids = GetRelationPublications(ancestor, true);
+					if (list_member_oid(aexceptpubids, puboid))
+						goto next;
+				}
+				else
+					goto next;
 			}
 		}
 
+		topmost_relid = ancestor;
+
+		if (ancestor_level)
+			*ancestor_level = level;
+
+next:
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +408,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +678,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +694,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +794,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +820,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +842,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1109,7 +1129,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 9ed8cdedbc..feba821fb7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+											  PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1193,6 +1198,77 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	Assert(!pubform->puballtables);
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1502,6 +1578,19 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1747,6 +1836,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1819,6 +1909,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1894,8 +1985,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..5d97eadf54 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16276,7 +16276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16413,7 +16413,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7e13666a2..0c75d145f1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,7 +455,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -588,6 +588,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10430,12 +10431,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10473,6 +10475,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10548,6 +10551,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10586,6 +10608,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 42c06af239..6394466dab 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1996,7 +1996,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2085,22 +2086,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2119,7 +2104,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2134,6 +2120,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2209,6 +2197,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 60e72f9e8b..4659c766dc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5561,6 +5561,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5594,7 +5596,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5608,14 +5610,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5644,7 +5651,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5661,7 +5668,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7cc9c72e49..2925d0f27a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -3980,8 +3982,35 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool		first = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			PublicationInfo *relpubinfo = pubrinfo->publication;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == relpubinfo)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4151,6 +4180,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4192,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4213,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4229,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4241,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4262,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4259,6 +4305,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9936,6 +9985,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17869,6 +17921,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1f08716f69..13a3b3f875 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,18 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1a5d924a23..78aa409f40 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2950,17 +2950,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6321,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND pr.prexcept = 'f'\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6321,6 +6345,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept = 't'\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62ecc3cdab..309c4b53be 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,9 +1822,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2986,7 +2990,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48205ba429..c92dd40a17 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9726fdae58..6de15c391b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 799a3f15f5..2696f9c1dc 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +214,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1656,9 +1697,14 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,9 +1721,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
@@ -1685,6 +1746,26 @@ ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA pu
  regress_publication_user | f          | t       | t       | t       | t         | f
 Tables:
     "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
 Tables from schemas:
     "public"
 
@@ -1696,13 +1777,40 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
--- Verify that publish options and publish_via_partition_root option are reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- can't add EXCEPT TABLE when the publication options are not the default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that publish option is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
 --------------------------+------------+---------+---------+---------+-----------+----------
- regress_publication_user | f          | f       | f       | f       | f         | t
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- can't add EXCEPT TABLE when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that publish_via_partition_root option is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
@@ -1721,6 +1829,7 @@ ERROR:  must be superuser to RESET publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 868f1c51b1..1145744465 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1060,26 +1073,63 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- can't add EXCEPT TABLE when the publication options are not the default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- Verify that publish option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- can't add EXCEPT TABLE when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that publish options and publish_via_partition_root option are reset
+-- Verify that publish_via_partition_root option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1092,6 +1142,7 @@ SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..cd76f5bc3d
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,85 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#52vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#48)
Re: Skipping schema changes in publication

On Fri, May 20, 2022 at 5:49 AM Peter Smith <smithpb2250@gmail.com> wrote:

FYI, although the v6-0002 patch applied cleanly, I found that the SGML
was malformed and so the pg docs build fails.

~~~
e.g.

[postgres@CentOS7-x64 sgml]$ make STYLE=website html
{ \
echo "<!ENTITY version \"15beta1\">"; \
echo "<!ENTITY majorversion \"15\">"; \
} > version.sgml
'/usr/bin/perl' ./mk_feature_tables.pl YES
../../../src/backend/catalog/sql_feature_packages.txt
../../../src/backend/catalog/sql_features.txt >
features-supported.sgml
'/usr/bin/perl' ./mk_feature_tables.pl NO
../../../src/backend/catalog/sql_feature_packages.txt
../../../src/backend/catalog/sql_features.txt >
features-unsupported.sgml
'/usr/bin/perl' ./generate-errcodes-table.pl
../../../src/backend/utils/errcodes.txt > errcodes-table.sgml
'/usr/bin/perl' ./generate-keywords-table.pl . > keywords-table.sgml
/usr/bin/xmllint --path . --noout --valid postgres.sgml
ref/create_publication.sgml:171: parser error : Opening and ending tag
mismatch: varlistentry line 166 and listitem
</listitem>
^
ref/create_publication.sgml:172: parser error : Opening and ending tag
mismatch: variablelist line 60 and varlistentry
</varlistentry>
^
ref/create_publication.sgml:226: parser error : Opening and ending tag
mismatch: refsect1 line 57 and variablelist
</variablelist>
^
...

I will work around it locally, but for future patches please check the
SGML builds ok before posting.

Thanks for reporting this, I have made the changes for this.
The v7 patch attached at [1]/messages/by-id/CALDaNm3EpX3+Ru=SNaYi=UW5ZLE6nNhGRHZ7a8-fXPZ_-gLdxQ@mail.gmail.com has the changes for the same.

[1]: /messages/by-id/CALDaNm3EpX3+Ru=SNaYi=UW5ZLE6nNhGRHZ7a8-fXPZ_-gLdxQ@mail.gmail.com

Regards,
Vignesh

#53vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#50)
Re: Skipping schema changes in publication

On Fri, May 20, 2022 at 11:23 AM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v6-0002.

======

1. Commit message.
The psql \d family of commands to display excluded tables.

SUGGESTION
The psql \d family of commands can now display excluded tables.

Modified

~~~

2. doc/src/sgml/ref/alter_publication.sgml

@@ -22,6 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD <replaceable class="parameter">publication_object</replaceable> [,
...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]

The "exception_object" font is wrong. Should look the same as
"publication_object"

Modified

~~~

3. doc/src/sgml/ref/alter_publication.sgml - Examples

@@ -214,6 +220,14 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
</programlisting>
</para>

+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT TABLE
users, departments;
+</programlisting></para>

Consider using "EXCEPT" instead of "EXCEPT TABLE" because that will
show TABLE keyword is optional.

Modified

~~~

4. doc/src/sgml/ref/create_publication.sgml

An SGML tag error caused building the docs to fail. My fix was
previously reported [1].

Modified

~~~

5. doc/src/sgml/ref/create_publication.sgml

@@ -22,7 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]

The "exception_object" font is wrong. Should look the same as
"publication_object"

Modified

~~~

6. doc/src/sgml/ref/create_publication.sgml - Examples

@@ -351,6 +366,15 @@ CREATE PUBLICATION production_publication FOR
TABLE users, departments, ALL TABL
CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
</programlisting></para>

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+

6a.
Typo: "FOR ALL TABLE" -> "FOR ALL TABLES"

Modified

6b.
Consider using "EXCEPT" instead of "EXCEPT TABLE" because that will
show TABLE keyword is optional.

Modified

~~~

7. src/backend/catalog/pg_publication.c - GetTopMostAncestorInPublication

@@ -316,18 +316,25 @@ GetTopMostAncestorInPublication(Oid puboid, List
*ancestors, int *ancestor_level
}
else
{
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
+ List    *aschemapubids = NIL;
+ List    *aexceptpubids = NIL;
+
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+ aexceptpubids = GetRelationPublications(ancestor, true);
+ if (list_member_oid(aschemapubids, puboid) ||
+ (puballtables && !list_member_oid(aexceptpubids, puboid)))
{

You could re-write this as multiple conditions instead of one. That
could avoid always assigning the 'aexceptpubids', so it might be a
more efficient way to write this logic.

Modified

~~~

8. src/backend/catalog/pg_publication.c - CheckPublicationDefValues

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * Publication is having default options
+ *  Publication is not associated with relations
+ *  Publication is not associated with schemas
+ *  Publication is not set with "FOR ALL TABLES"
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

8a.
Remove the tab. Replace with spaces.

Modified

8b.
It might be better if this comment order is the same as the logic order.
e.g.

* Check the following:
* Publication is not set with "FOR ALL TABLES"
* Publication is having default options
* Publication is not associated with schemas
* Publication is not associated with relations

Modified

~~~

9. src/backend/catalog/pg_publication.c - AlterPublicationSetAllTables

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and
publication schemas.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)

The function comment and the function name do not seem to match here;
something looks like a cut/paste error ??

Modified

~~~

10. src/backend/catalog/pg_publication.c - AlterPublicationSetAllTables

+ /* set all tables option */
+ values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+ replaces[Anum_pg_publication_puballtables - 1] = true;

SUGGEST (comment)
/* set all ALL TABLES flag */

Modified

~~~

11. src/backend/catalog/pg_publication.c - AlterPublication

@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg should start with a lowercase letter.

Modified

~~~

12. src/backend/catalog/pg_publication.c - AlterPublication

@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

Example test:

postgres=# create table t1(a int);
CREATE TABLE
postgres=# create publication p1 for table t1;
CREATE PUBLICATION
postgres=# alter publication p1 add all tables except t1;
2022-05-20 14:34:49.301 AEST [21802] ERROR: Setting ALL TABLES
requires publication "p1" to have default values
2022-05-20 14:34:49.301 AEST [21802] HINT: Use ALTER PUBLICATION ...
RESET to reset the publication
2022-05-20 14:34:49.301 AEST [21802] STATEMENT: alter publication p1
add all tables except t1;
ERROR: Setting ALL TABLES requires publication "p1" to have default values
HINT: Use ALTER PUBLICATION ... RESET to reset the publication
postgres=# alter publication p1 set all tables except t1;

That error message does not quite match what the user was doing.
Firstly, they were adding the ALL TABLES, not setting it. Secondly,
all the values of the publication were already defaults (only there
was an existing table t1 in the publication). Maybe some minor changes
to the message wording can be a better reflect what the user is doing
here.

Modified

~~~

13. src/backend/parser/gram.y

@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE
aggregate_with_argtypes OWNER TO RoleSpec
*
* CREATE PUBLICATION name [WITH options]
*
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT TABLE table [, ...]]
[WITH options]

Comment should show the "TABLE" keyword is optional

Modified

~~~

14. src/bin/pg_dump/pg_dump.c - dumpPublicationTable

@@ -4332,6 +4380,7 @@ dumpPublicationTable(Archive *fout, const
PublicationRelInfo *pubrinfo)

appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
fmtId(pubinfo->dobj.name));
+
appendPQExpBuffer(query, " %s",
fmtQualifiedDumpable(tbinfo));

This additional whitespace seems unrelated to this patch

Modified

~~~

15. src/include/nodes/parsenodes.h

15a.
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
RangeVar *relation; /* relation to be published */
Node *whereClause; /* qualifications */
List *columns; /* List of columns in a publication table */
+ bool except; /* except relation */
} PublicationTable;

Maybe the comment should be more like similar ones:
/* exclude the relation */

Modified

15b.
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
typedef enum PublicationObjSpecType
{
PUBLICATIONOBJ_TABLE, /* A table */
+ PUBLICATIONOBJ_EXCEPT_TABLE, /* An Except table */
PUBLICATIONOBJ_TABLES_IN_SCHEMA, /* All tables in schema */
PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA, /* All tables in first element of

Maybe the comment should be more like:
/* A table to be excluded */

Modified

~~~

16. src/test/regress/sql/publication.sql

I did not see any test cases using EXCEPT when the optional TABLE
keyword is omitted.

Added a test

Thanks for the comments, the v7 patch attached at [1]/messages/by-id/CALDaNm3EpX3+Ru=SNaYi=UW5ZLE6nNhGRHZ7a8-fXPZ_-gLdxQ@mail.gmail.com has the changes
for the same.
[1]: /messages/by-id/CALDaNm3EpX3+Ru=SNaYi=UW5ZLE6nNhGRHZ7a8-fXPZ_-gLdxQ@mail.gmail.com

Regards,
Vignesh

#54vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#53)
2 attachment(s)
Re: Skipping schema changes in publication

On Sat, May 21, 2022 at 11:06 AM vignesh C <vignesh21@gmail.com> wrote:

On Fri, May 20, 2022 at 11:23 AM Peter Smith <smithpb2250@gmail.com> wrote:

Below are my review comments for v6-0002.

======

1. Commit message.
The psql \d family of commands to display excluded tables.

SUGGESTION
The psql \d family of commands can now display excluded tables.

Modified

~~~

2. doc/src/sgml/ref/alter_publication.sgml

@@ -22,6 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD <replaceable class="parameter">publication_object</replaceable> [,
...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]

The "exception_object" font is wrong. Should look the same as
"publication_object"

Modified

~~~

3. doc/src/sgml/ref/alter_publication.sgml - Examples

@@ -214,6 +220,14 @@ ALTER PUBLICATION sales_publication ADD ALL
TABLES IN SCHEMA marketing, sales;
</programlisting>
</para>

+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT TABLE
users, departments;
+</programlisting></para>

Consider using "EXCEPT" instead of "EXCEPT TABLE" because that will
show TABLE keyword is optional.

Modified

~~~

4. doc/src/sgml/ref/create_publication.sgml

An SGML tag error caused building the docs to fail. My fix was
previously reported [1].

Modified

~~~

5. doc/src/sgml/ref/create_publication.sgml

@@ -22,7 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] exception_object [, ... ] ]

The "exception_object" font is wrong. Should look the same as
"publication_object"

Modified

~~~

6. doc/src/sgml/ref/create_publication.sgml - Examples

@@ -351,6 +366,15 @@ CREATE PUBLICATION production_publication FOR
TABLE users, departments, ALL TABL
CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
</programlisting></para>

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> table:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLE EXCEPT TABLE users, departments;
+</programlisting>
+  </para>
+

6a.
Typo: "FOR ALL TABLE" -> "FOR ALL TABLES"

Modified

6b.
Consider using "EXCEPT" instead of "EXCEPT TABLE" because that will
show TABLE keyword is optional.

Modified

~~~

7. src/backend/catalog/pg_publication.c - GetTopMostAncestorInPublication

@@ -316,18 +316,25 @@ GetTopMostAncestorInPublication(Oid puboid, List
*ancestors, int *ancestor_level
}
else
{
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
+ List    *aschemapubids = NIL;
+ List    *aexceptpubids = NIL;
+
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+ aexceptpubids = GetRelationPublications(ancestor, true);
+ if (list_member_oid(aschemapubids, puboid) ||
+ (puballtables && !list_member_oid(aexceptpubids, puboid)))
{

You could re-write this as multiple conditions instead of one. That
could avoid always assigning the 'aexceptpubids', so it might be a
more efficient way to write this logic.

Modified

~~~

8. src/backend/catalog/pg_publication.c - CheckPublicationDefValues

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * Publication is having default options
+ *  Publication is not associated with relations
+ *  Publication is not associated with schemas
+ *  Publication is not set with "FOR ALL TABLES"
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

8a.
Remove the tab. Replace with spaces.

Modified

8b.
It might be better if this comment order is the same as the logic order.
e.g.

* Check the following:
* Publication is not set with "FOR ALL TABLES"
* Publication is having default options
* Publication is not associated with schemas
* Publication is not associated with relations

Modified

~~~

9. src/backend/catalog/pg_publication.c - AlterPublicationSetAllTables

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, publication relations and
publication schemas.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)

The function comment and the function name do not seem to match here;
something looks like a cut/paste error ??

Modified

~~~

10. src/backend/catalog/pg_publication.c - AlterPublicationSetAllTables

+ /* set all tables option */
+ values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+ replaces[Anum_pg_publication_puballtables - 1] = true;

SUGGEST (comment)
/* set all ALL TABLES flag */

Modified

~~~

11. src/backend/catalog/pg_publication.c - AlterPublication

@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg should start with a lowercase letter.

Modified

~~~

12. src/backend/catalog/pg_publication.c - AlterPublication

@@ -1501,6 +1579,20 @@ AlterPublication(ParseState *pstate,
AlterPublicationStmt *stmt)
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
stmt->pubname);

+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("Setting ALL TABLES requires publication \"%s\" to have
default values",
+    stmt->pubname),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

Example test:

postgres=# create table t1(a int);
CREATE TABLE
postgres=# create publication p1 for table t1;
CREATE PUBLICATION
postgres=# alter publication p1 add all tables except t1;
2022-05-20 14:34:49.301 AEST [21802] ERROR: Setting ALL TABLES
requires publication "p1" to have default values
2022-05-20 14:34:49.301 AEST [21802] HINT: Use ALTER PUBLICATION ...
RESET to reset the publication
2022-05-20 14:34:49.301 AEST [21802] STATEMENT: alter publication p1
add all tables except t1;
ERROR: Setting ALL TABLES requires publication "p1" to have default values
HINT: Use ALTER PUBLICATION ... RESET to reset the publication
postgres=# alter publication p1 set all tables except t1;

That error message does not quite match what the user was doing.
Firstly, they were adding the ALL TABLES, not setting it. Secondly,
all the values of the publication were already defaults (only there
was an existing table t1 in the publication). Maybe some minor changes
to the message wording can be a better reflect what the user is doing
here.

Modified

~~~

13. src/backend/parser/gram.y

@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE
aggregate_with_argtypes OWNER TO RoleSpec
*
* CREATE PUBLICATION name [WITH options]
*
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT TABLE table [, ...]]
[WITH options]

Comment should show the "TABLE" keyword is optional

Modified

~~~

14. src/bin/pg_dump/pg_dump.c - dumpPublicationTable

@@ -4332,6 +4380,7 @@ dumpPublicationTable(Archive *fout, const
PublicationRelInfo *pubrinfo)

appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
fmtId(pubinfo->dobj.name));
+
appendPQExpBuffer(query, " %s",
fmtQualifiedDumpable(tbinfo));

This additional whitespace seems unrelated to this patch

Modified

~~~

15. src/include/nodes/parsenodes.h

15a.
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
RangeVar *relation; /* relation to be published */
Node *whereClause; /* qualifications */
List *columns; /* List of columns in a publication table */
+ bool except; /* except relation */
} PublicationTable;

Maybe the comment should be more like similar ones:
/* exclude the relation */

Modified

15b.
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
typedef enum PublicationObjSpecType
{
PUBLICATIONOBJ_TABLE, /* A table */
+ PUBLICATIONOBJ_EXCEPT_TABLE, /* An Except table */
PUBLICATIONOBJ_TABLES_IN_SCHEMA, /* All tables in schema */
PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA, /* All tables in first element of

Maybe the comment should be more like:
/* A table to be excluded */

Modified

~~~

16. src/test/regress/sql/publication.sql

I did not see any test cases using EXCEPT when the optional TABLE
keyword is omitted.

Added a test

Thanks for the comments, the v7 patch attached at [1] has the changes
for the same.
[1] - /messages/by-id/CALDaNm3EpX3+Ru=SNaYi=UW5ZLE6nNhGRHZ7a8-fXPZ_-gLdxQ@mail.gmail.com

Attached v7 patch which fixes the buildfarm warning for an unused
warning in release mode as in [1]https://cirrus-ci.com/task/6220288017825792.
[1]: https://cirrus-ci.com/task/6220288017825792

Regards,
Vignesh

Attachments:

v7-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v7-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 425c35981b50f574c2b5b109e07b548853afede0 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 17 May 2022 11:50:00 +0530
Subject: [PATCH v7 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   5 +-
 doc/src/sgml/ref/alter_publication.sgml       |  18 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  71 ++++---
 src/backend/commands/publicationcmds.c        | 179 +++++++++++++-----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  39 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  57 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  12 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     | 125 +++++++++++-
 src/test/regress/sql/publication.sql          |  65 ++++++-
 .../t/032_rep_changes_except_table.pl         |  85 +++++++++
 24 files changed, 702 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d96c72e531..985eda16a7 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6426,6 +6426,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..d7d6ba0529 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   <para>
    To add tables to a publication, the user must have ownership rights on the
    table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 47bd15f1fa..82a4ea4ec1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -214,6 +220,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..92916bf72c 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +367,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5fc6b1034a..3889796b3f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 8c7fca62de..be8282a3c3 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,43 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		if (!list_member_oid(apubids, puboid))
 		{
-			topmost_relid = ancestor;
-
-			if (ancestor_level)
-				*ancestor_level = level;
-		}
-		else
-		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
+			/* check if member of schema publications */
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+			if (!list_member_oid(aschemapubids, puboid))
 			{
-				topmost_relid = ancestor;
-
-				if (ancestor_level)
-					*ancestor_level = level;
+				/*
+				 * If the publication is all tables publication and the table
+				 * is not part of exception tables.
+				 */
+				if (puballtables)
+				{
+					aexceptpubids = GetRelationPublications(ancestor, true);
+					if (list_member_oid(aexceptpubids, puboid))
+						goto next;
+				}
+				else
+					goto next;
 			}
 		}
 
+		topmost_relid = ancestor;
+
+		if (ancestor_level)
+			*ancestor_level = level;
+
+next:
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +408,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +678,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +694,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +794,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +820,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +842,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1109,7 +1129,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 9ed8cdedbc..b7ee0906ce 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+											  PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1193,6 +1198,81 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+#ifdef USE_ASSERT_CHECKING
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+#endif
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1502,6 +1582,19 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1747,6 +1840,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1819,6 +1913,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1894,8 +1989,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..5d97eadf54 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16276,7 +16276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16413,7 +16413,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7e13666a2..0c75d145f1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,7 +455,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -588,6 +588,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10430,12 +10431,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10473,6 +10475,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10548,6 +10551,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10586,6 +10608,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 42c06af239..6394466dab 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1996,7 +1996,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2085,22 +2086,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2119,7 +2104,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2134,6 +2120,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2209,6 +2197,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 60e72f9e8b..4659c766dc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5561,6 +5561,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5594,7 +5596,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5608,14 +5610,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5644,7 +5651,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5661,7 +5668,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7cc9c72e49..2925d0f27a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -3980,8 +3982,35 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool		first = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			PublicationInfo *relpubinfo = pubrinfo->publication;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == relpubinfo)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4151,6 +4180,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4192,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4213,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4229,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4241,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4262,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4259,6 +4305,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9936,6 +9985,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17869,6 +17921,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1f08716f69..13a3b3f875 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,18 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1a5d924a23..78aa409f40 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2950,17 +2950,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND pr.prexcept = 'f'\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6321,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND pr.prexcept = 'f'\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6321,6 +6345,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept = 't'\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62ecc3cdab..309c4b53be 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,9 +1822,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2986,7 +2990,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48205ba429..c92dd40a17 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9726fdae58..6de15c391b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 799a3f15f5..2696f9c1dc 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +214,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1656,9 +1697,14 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,9 +1721,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
@@ -1685,6 +1746,26 @@ ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA pu
  regress_publication_user | f          | t       | t       | t       | t         | f
 Tables:
     "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
 Tables from schemas:
     "public"
 
@@ -1696,13 +1777,40 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
--- Verify that publish options and publish_via_partition_root option are reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- can't add EXCEPT TABLE when the publication options are not the default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that publish option is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
 --------------------------+------------+---------+---------+---------+-----------+----------
- regress_publication_user | f          | f       | f       | f       | f         | t
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- can't add EXCEPT TABLE when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that publish_via_partition_root option is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
@@ -1721,6 +1829,7 @@ ERROR:  must be superuser to RESET publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 868f1c51b1..1145744465 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1060,26 +1073,63 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- can't add EXCEPT TABLE when the publication options are not the default
+-- values
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that tables and schemas associated with the publication are dropped
--- after RESET
+-- Verify that publish option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- can't add EXCEPT TABLE when publish_via_partition_root option does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
--- Verify that publish options and publish_via_partition_root option are reset
+-- Verify that publish_via_partition_root option is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1092,6 +1142,7 @@ SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..cd76f5bc3d
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,85 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

v7-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 7fa257e1dbe255db0ef9379ecb5d71f9b1e15331 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v7 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
options, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  38 ++++++--
 src/backend/commands/publicationcmds.c    | 100 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out |  69 +++++++++++++++
 src/test/regress/sql/publication.sql      |  37 ++++++++
 7 files changed, 242 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e2cce49471..47bd15f1fa 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -207,6 +221,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 8e645741e4..9ed8cdedbc 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and options */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,86 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication options */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1504,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 989db0dbec..d7e13666a2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10558,6 +10558,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10604,6 +10606,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 55af9eb04e..62ecc3cdab 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1819,7 +1819,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 73f635b455..9726fdae58 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4035,7 +4035,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 274b37dfe5..799a3f15f5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,75 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..868f1c51b1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,43 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, ALL TABLES IN SCHEMA public;
+
+-- Verify that tables and schemas associated with the publication are dropped
+-- after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '', PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

#55osumi.takamichi@fujitsu.com
osumi.takamichi@fujitsu.com
In reply to: vignesh C (#54)
RE: Skipping schema changes in publication

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

(5) typo

   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

(6) Minor suggestion for an expression change

       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Best Regards,
Takamichi Osumi

#56Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#54)
Re: Skipping schema changes in publication

Here are some minor review comments for v7-0001.

======

1. General

Probably the commit message and all the PG docs and code comments
should be changed to refer to "publication parameters" instead of
(currently) "publication options". This is because these things are
really called "publication_parameters" in the PG docs [1]https://www.postgresql.org/docs/current/sql-createpublication.html.

All the following review comments are just examples of this suggestion.

~~~

2. Commit message

"includes resetting the publication options," -> "includes resetting
the publication parameters,"

~~~

3. doc/src/sgml/ref/alter_publication.sgml

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>

"resetting the publication options," -> "resetting the publication parameters,"

~~~

4. src/backend/commands/publicationcmds.c

@@ -53,6 +53,14 @@
#include "utils/syscache.h"
#include "utils/varlena.h"

+/* CREATE PUBLICATION default values for flags and options */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false

"flags and options" -> "flags and publication parameters"

~~~

5. src/backend/commands/publicationcmds.c

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+   Relation rel, HeapTuple tup)

"Reset the publication options," -> "Reset the publication parameters,"

~~~

6. src/test/regress/sql/publication.sql

+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset

SUGGESTION
-- Verify that 'publish' and 'publish_via_partition_root' publication
parameters are reset

------
[1]: https://www.postgresql.org/docs/current/sql-createpublication.html

Kind Regards,
Peter Smith.
Fujitsu Australia

#57Peter Smith
smithpb2250@gmail.com
In reply to: vignesh C (#54)
Re: Skipping schema changes in publication

Here are my review comments for patch v7-0002.

======

1. doc/src/sgml/logical-replication.sgml

@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication
origin "pg_16395" during "INSER
   <para>
    To add tables to a publication, the user must have ownership rights on the
    table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
   </para>

I felt that maybe this whole paragraph should be rearranged. Put the
"create publication" parts before the "alter publication" parts;
Re-word the sentences more similarly. I also felt the ALL TABLES and
ALL TABLES IN SCHEMA etc should be written uppercase/literal since
that is what was meant.

SUGGESTION
To create a publication using FOR ALL TABLES or FOR ALL TABLES IN
SCHEMA, the user must be a superuser. To add ALL TABLES or ALL TABLES
IN SCHEMA to a publication, the user must be a superuser. To add
tables to a publication, the user must have ownership rights on the
table.

~~~

2. doc/src/sgml/ref/alter_publication.sgml

@@ -82,8 +88,8 @@ ALTER PUBLICATION <replaceable
class="parameter">name</replaceable> RESET

   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and

Isn't this missing some information that says ADD ALL TABLES requires
the invoking user to be a superuser?

~~~

3. doc/src/sgml/ref/alter_publication.sgml - examples

+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users,
departments;
+</programlisting></para>
+

I didn't think it needs to say "tables" 2x (e.g. remove the last "tables")

~~~

4. doc/src/sgml/ref/create_publication.sgml - examples

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>

I didn't think it needs to say "tables" 2x (e.g. remove the last "tables")

~~~

5. src/backend/catalog/pg_publication.c

  foreach(lc, ancestors)
  {
  Oid ancestor = lfirst_oid(lc);
- List    *apubids = GetRelationPublications(ancestor);
- List    *aschemaPubids = NIL;
+ List    *apubids = GetRelationPublications(ancestor, false);
+ List    *aschemapubids = NIL;
+ List    *aexceptpubids = NIL;

level++;

- if (list_member_oid(apubids, puboid))
+ /* check if member of table publications */
+ if (!list_member_oid(apubids, puboid))
  {
- topmost_relid = ancestor;
-
- if (ancestor_level)
- *ancestor_level = level;
- }
- else
- {
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
+ /* check if member of schema publications */
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+ if (!list_member_oid(aschemapubids, puboid))
  {
- topmost_relid = ancestor;
-
- if (ancestor_level)
- *ancestor_level = level;
+ /*
+ * If the publication is all tables publication and the table
+ * is not part of exception tables.
+ */
+ if (puballtables)
+ {
+ aexceptpubids = GetRelationPublications(ancestor, true);
+ if (list_member_oid(aexceptpubids, puboid))
+ goto next;
+ }
+ else
+ goto next;
  }
  }
+ topmost_relid = ancestor;
+
+ if (ancestor_level)
+ *ancestor_level = level;
+
+next:
  list_free(apubids);
- list_free(aschemaPubids);
+ list_free(aschemapubids);
+ list_free(aexceptpubids);
  }

I felt those negative (!) conditions and those goto are making this
logic hard to understand. Can’t it be simplified more than this? Even
just having another bool flag might help make it easier.

e.g. Perhaps something a bit like this (but add some comments)

foreach(lc, ancestors)
{
Oid ancestor = lfirst_oid(lc);
List *apubids = GetRelationPublications(ancestor);
List *aschemaPubids = NIL;
List *aexceptpubids = NIL;
bool set_top = false;
level++;

set_top = list_member_oid(apubids, puboid);
if (!set_top)
{
aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
set_top = list_member_oid(aschemaPubids, puboid);

if (!set_top && puballtables)
{
aexceptpubids = GetRelationPublications(ancestor, true);
set_top = !list_member_oid(aexceptpubids, puboid);
}
}
if (set_top)
{
topmost_relid = ancestor;

if (ancestor_level)
*ancestor_level = level;
}

list_free(apubids);
list_free(aschemapubids);
list_free(aexceptpubids);
}

------

6. src/backend/commands/publicationcmds.c - CheckPublicationDefValues

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think Osumi-san already gave a review [1]/messages/by-id/TYCPR01MB83730A2F1D6A5303E9C1416AEDD99@TYCPR01MB8373.jpnprd01.prod.outlook.com about this same comment.

So I only wanted to add that it should not say "options" here:
"default options" -> "default publication parameter values"

~~~

7. src/backend/commands/publicationcmds.c - AlterPublicationSetAllTables

+#ifdef USE_ASSERT_CHECKING
+ Assert(!pubform->puballtables);
+#endif

Why is this #ifdef needed? Isn't that logic built into the Assert macro already?

~~~

8. src/backend/commands/publicationcmds.c - AlterPublicationSetAllTables

+ /* set ALL TABLES flag */

Use uppercase 'S' to match other comments.

~~~

9. src/backend/commands/publicationcmds.c - AlterPublication

+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("adding ALL TABLES requires the publication to have default
publication options, no tables/schemas associated and ALL TABLES flag
should not be set"),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

IMO this errmsg text is not very good but I think Osumi-san [1]/messages/by-id/TYCPR01MB83730A2F1D6A5303E9C1416AEDD99@TYCPR01MB8373.jpnprd01.prod.outlook.com has
also given a review comment about the same errmsg.

So I only wanted to add that should not say "options" here:
"default publication options" -> "default publication parameter values"

~~~

10. src/backend/parser/gram.y

/*****************************************************************************
*
* ALTER PUBLICATION name SET ( options )
*
* ALTER PUBLICATION name ADD pub_obj [, ...]
*
* ALTER PUBLICATION name DROP pub_obj [, ...]
*
* ALTER PUBLICATION name SET pub_obj [, ...]
*
* ALTER PUBLICATION name RESET
*
* pub_obj is one of:
*
* TABLE table_name [, ...]
* ALL TABLES IN SCHEMA schema_name [, ...]
*
*****************************************************************************/

-

Should the above comment be updated to mention also ADD ALL TABLES
... EXCEPT [TABLE] ...

~~~

11. src/bin/pg_dump/pg_dump.c - dumpPublication

+ /* Include exception tables if the publication has except tables */
+ for (cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ PublicationInfo *relpubinfo = pubrinfo->publication;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == relpubinfo)

I am unsure if that variable 'relpubinfo' is of much use; it is only
used one time.

~~~

12. src/bin/pg_dump/t/002_pg_dump.pl

I think there should be more test cases here:

E.g.1. EXCEPT TABLE should also test a list of tables

E.g.2. EXCEPT with optional TABLE keyword ommitted

~~~

13. src/bin/psql/describe.c - question about the SQL

Since the new 'except' is a boolean column, wouldn't it be more
natural if all the SQL was treating it as one?

e.g. should the SQL be saying "IS preexpect", "IS NOT prexcept";
instead of comparing preexpect to 't' and 'f' character.

~~~

14. .../t/032_rep_changes_except_table.pl

+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher

"option" -> "clause"

------
[1]: /messages/by-id/TYCPR01MB83730A2F1D6A5303E9C1416AEDD99@TYCPR01MB8373.jpnprd01.prod.outlook.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#58vignesh C
vignesh21@gmail.com
In reply to: osumi.takamichi@fujitsu.com (#55)
2 attachment(s)
Re: Skipping schema changes in publication

On Thu, May 26, 2022 at 7:04 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

CacheInvalidateRelcacheAll should be called if we change all tables
from true to false, else the cache will not be invalidated. Modified

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

Added errdetail and split it

(5) typo

<varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

Modified

(6) Minor suggestion for an expression change

Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

I felt the existing is better.

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

Modified

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Changed it to "Publication is having default publication parameter values"

Thanks for the comments, the attached v8 patch has the changes for the same.

Regards,
Vignesh

Attachments:

v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 6b7a826aa9a485401f13d8770749edcb49bb4f5b Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Fri, 3 Jun 2022 15:20:53 +0530
Subject: [PATCH v8 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 199 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 .../t/032_rep_changes_except_table.pl         |  85 ++++++++
 24 files changed, 693 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c00c93dd7b..d983c5fed7 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6436,6 +6436,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 145ea71d61..f40348c058 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1165,10 +1165,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d4c23debd1..0ee2aa27a5 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -224,6 +231,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1a828e8d2f..0f16bbf2b7 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -351,6 +367,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 47bf3342a5..a1e49c3d8d 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1868,8 +1868,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 8c7fca62de..0419deee68 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1109,7 +1130,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 533071d911..bc499fc29f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+											  PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1160,6 +1165,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1198,6 +1224,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1507,6 +1606,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1752,6 +1865,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1824,6 +1938,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1899,8 +2014,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..5d97eadf54 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16276,7 +16276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16413,7 +16413,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6d2ee2ee92..248496b1dc 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,7 +455,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -588,6 +588,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10410,7 +10411,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10430,12 +10431,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10473,6 +10475,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10548,6 +10551,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10560,6 +10582,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10586,6 +10610,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 8deae57143..2193b3dac6 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1996,7 +1996,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2085,22 +2086,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2119,7 +2104,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2134,6 +2120,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2209,6 +2197,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 60e72f9e8b..4659c766dc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5561,6 +5561,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5594,7 +5596,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5608,14 +5610,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5644,7 +5651,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5661,7 +5668,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7cc9c72e49..06f83a8010 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -3980,8 +3982,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool		first = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4151,6 +4179,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4162,8 +4191,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4174,6 +4212,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4189,6 +4228,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4200,6 +4240,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4220,7 +4261,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4259,6 +4304,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9936,6 +9984,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17869,6 +17920,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..07a50ece68 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1f08716f69..a06d6c976b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1a5d924a23..98b0ad924f 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2950,17 +2950,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6302,8 +6321,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6321,6 +6345,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index f76ebdac2c..130bf7a63e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1822,9 +1822,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2986,7 +2990,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48205ba429..c92dd40a17 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9726fdae58..6de15c391b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3999,6 +3999,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4007,6 +4008,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5d4db2507e..fc2fa4a293 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +214,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1656,9 +1697,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,7 +1722,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1694,6 +1758,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1712,6 +1781,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1729,6 +1804,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1750,9 +1831,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; 
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 72765994dd..a435e1df2b 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1060,17 +1073,30 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1078,6 +1104,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1085,6 +1114,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1092,6 +1125,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1101,10 +1138,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; 
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..7156e39fcd
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,85 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Also wait for initial table sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 423328113591477a46447f3e22f72367f52512ef Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v8 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  38 ++++++--
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 292 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 3e338f4cc5..d4c23debd1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -217,6 +231,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 8e645741e4..533071d911 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1509,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 969c9c158f..6d2ee2ee92 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10558,6 +10558,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10604,6 +10606,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index e1cc753489..f76ebdac2c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1819,7 +1819,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 73f635b455..9726fdae58 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4035,7 +4035,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 274b37dfe5..5d4db2507e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..72765994dd 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

#59vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#56)
Re: Skipping schema changes in publication

'On Mon, May 30, 2022 at 1:51 PM Peter Smith <smithpb2250@gmail.com> wrote:

Here are some minor review comments for v7-0001.

======

1. General

Probably the commit message and all the PG docs and code comments
should be changed to refer to "publication parameters" instead of
(currently) "publication options". This is because these things are
really called "publication_parameters" in the PG docs [1].

All the following review comments are just examples of this suggestion.

Modified

~~~

2. Commit message

"includes resetting the publication options," -> "includes resetting
the publication parameters,"

Modified

~~~

3. doc/src/sgml/ref/alter_publication.sgml

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
</para>

"resetting the publication options," -> "resetting the publication parameters,"

Modified

~~~

4. src/backend/commands/publicationcmds.c

@@ -53,6 +53,14 @@
#include "utils/syscache.h"
#include "utils/varlena.h"

+/* CREATE PUBLICATION default values for flags and options */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false

"flags and options" -> "flags and publication parameters"

Modified

~~~

5. src/backend/commands/publicationcmds.c

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+   Relation rel, HeapTuple tup)

"Reset the publication options," -> "Reset the publication parameters,"

Modified

~~~

6. src/test/regress/sql/publication.sql

+-- Verify that publish options and publish_via_partition_root option are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset

SUGGESTION
-- Verify that 'publish' and 'publish_via_partition_root' publication
parameters are reset

Modified, I have split this into two tests as it will help the 0002
patch to add few tests with the existing steps for 'publish' and
'publish_via_partition_root' publication parameter.

Thanks for the comments. the v8 patch attached at [1]/messages/by-id/CALDaNm0sAU4s1KTLOEWv=rYo5dQK6uFTJn_0FKj3XG1Nv4D-qw@mail.gmail.com has the fixes
for the same.
[1]: /messages/by-id/CALDaNm0sAU4s1KTLOEWv=rYo5dQK6uFTJn_0FKj3XG1Nv4D-qw@mail.gmail.com

Regards,
Vignesh

#60vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#57)
Re: Skipping schema changes in publication

On Tue, May 31, 2022 at 11:51 AM Peter Smith <smithpb2250@gmail.com> wrote:

Here are my review comments for patch v7-0002.

======

1. doc/src/sgml/logical-replication.sgml

@@ -1167,8 +1167,9 @@ CONTEXT:  processing remote data for replication
origin "pg_16395" during "INSER
<para>
To add tables to a publication, the user must have ownership rights on the
table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   superuser. To add all tables to a publication, the user must be a superuser.
+   To create a publication that publishes all tables or all tables in schema
+   automatically, the user must be a superuser.
</para>

I felt that maybe this whole paragraph should be rearranged. Put the
"create publication" parts before the "alter publication" parts;
Re-word the sentences more similarly. I also felt the ALL TABLES and
ALL TABLES IN SCHEMA etc should be written uppercase/literal since
that is what was meant.

SUGGESTION
To create a publication using FOR ALL TABLES or FOR ALL TABLES IN
SCHEMA, the user must be a superuser. To add ALL TABLES or ALL TABLES
IN SCHEMA to a publication, the user must be a superuser. To add
tables to a publication, the user must have ownership rights on the
table.

Modified

~~~

2. doc/src/sgml/ref/alter_publication.sgml

@@ -82,8 +88,8 @@ ALTER PUBLICATION <replaceable
class="parameter">name</replaceable> RESET

<para>
You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES IN SCHEMA</literal>,
<literal>SET ALL TABLES IN SCHEMA</literal> to a publication and

Isn't this missing some information that says ADD ALL TABLES requires
the invoking user to be a superuser?

Modified

~~~

3. doc/src/sgml/ref/alter_publication.sgml - examples

+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users,
departments;
+</programlisting></para>
+

I didn't think it needs to say "tables" 2x (e.g. remove the last "tables")

Modified

~~~

4. doc/src/sgml/ref/create_publication.sgml - examples

+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname> tables:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>

I didn't think it needs to say "tables" 2x (e.g. remove the last "tables")

Modified

~~~

5. src/backend/catalog/pg_publication.c

foreach(lc, ancestors)
{
Oid ancestor = lfirst_oid(lc);
- List    *apubids = GetRelationPublications(ancestor);
- List    *aschemaPubids = NIL;
+ List    *apubids = GetRelationPublications(ancestor, false);
+ List    *aschemapubids = NIL;
+ List    *aexceptpubids = NIL;

level++;

- if (list_member_oid(apubids, puboid))
+ /* check if member of table publications */
+ if (!list_member_oid(apubids, puboid))
{
- topmost_relid = ancestor;
-
- if (ancestor_level)
- *ancestor_level = level;
- }
- else
- {
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
+ /* check if member of schema publications */
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
+ if (!list_member_oid(aschemapubids, puboid))
{
- topmost_relid = ancestor;
-
- if (ancestor_level)
- *ancestor_level = level;
+ /*
+ * If the publication is all tables publication and the table
+ * is not part of exception tables.
+ */
+ if (puballtables)
+ {
+ aexceptpubids = GetRelationPublications(ancestor, true);
+ if (list_member_oid(aexceptpubids, puboid))
+ goto next;
+ }
+ else
+ goto next;
}
}
+ topmost_relid = ancestor;
+
+ if (ancestor_level)
+ *ancestor_level = level;
+
+next:
list_free(apubids);
- list_free(aschemaPubids);
+ list_free(aschemapubids);
+ list_free(aexceptpubids);
}

I felt those negative (!) conditions and those goto are making this
logic hard to understand. Can’t it be simplified more than this? Even
just having another bool flag might help make it easier.

e.g. Perhaps something a bit like this (but add some comments)

foreach(lc, ancestors)
{
Oid ancestor = lfirst_oid(lc);
List *apubids = GetRelationPublications(ancestor);
List *aschemaPubids = NIL;
List *aexceptpubids = NIL;
bool set_top = false;
level++;

set_top = list_member_oid(apubids, puboid);
if (!set_top)
{
aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
set_top = list_member_oid(aschemaPubids, puboid);

if (!set_top && puballtables)
{
aexceptpubids = GetRelationPublications(ancestor, true);
set_top = !list_member_oid(aexceptpubids, puboid);
}
}
if (set_top)
{
topmost_relid = ancestor;

if (ancestor_level)
*ancestor_level = level;
}

list_free(apubids);
list_free(aschemapubids);
list_free(aexceptpubids);
}

Modified

------

6. src/backend/commands/publicationcmds.c - CheckPublicationDefValues

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think Osumi-san already gave a review [1] about this same comment.

So I only wanted to add that it should not say "options" here:
"default options" -> "default publication parameter values"

Modified

~~~

7. src/backend/commands/publicationcmds.c - AlterPublicationSetAllTables

+#ifdef USE_ASSERT_CHECKING
+ Assert(!pubform->puballtables);
+#endif

Why is this #ifdef needed? Isn't that logic built into the Assert macro already?

pubform is used only for assert case. If we don't use it within #ifdef
or PG_USED_FOR_ASSERTS_ONLY, it will throw a unused variable error
without --enable-cassert like:

publicationcmds.c: In function ‘AlterPublicationSetAllTables’:
publicationcmds.c:1250:29: error: unused variable ‘pubform’
[-Werror=unused-variable]
1250 | Form_pg_publication pubform = (Form_pg_publication)
GETSTRUCT(tup);
| ^~~~~~~
cc1: all warnings being treated as errors

~~~

8. src/backend/commands/publicationcmds.c - AlterPublicationSetAllTables

+ /* set ALL TABLES flag */

Use uppercase 'S' to match other comments.

Modified

~~~

9. src/backend/commands/publicationcmds.c - AlterPublication

+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("adding ALL TABLES requires the publication to have default
publication options, no tables/schemas associated and ALL TABLES flag
should not be set"),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

IMO this errmsg text is not very good but I think Osumi-san [1] has
also given a review comment about the same errmsg.

So I only wanted to add that should not say "options" here:
"default publication options" -> "default publication parameter values"

Modified

~~~

10. src/backend/parser/gram.y

/*****************************************************************************
*
* ALTER PUBLICATION name SET ( options )
*
* ALTER PUBLICATION name ADD pub_obj [, ...]
*
* ALTER PUBLICATION name DROP pub_obj [, ...]
*
* ALTER PUBLICATION name SET pub_obj [, ...]
*
* ALTER PUBLICATION name RESET
*
* pub_obj is one of:
*
* TABLE table_name [, ...]
* ALL TABLES IN SCHEMA schema_name [, ...]
*
*****************************************************************************/

-

Should the above comment be updated to mention also ADD ALL TABLES
... EXCEPT [TABLE] ...

Modified

~~~

11. src/bin/pg_dump/pg_dump.c - dumpPublication

+ /* Include exception tables if the publication has except tables */
+ for (cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ PublicationInfo *relpubinfo = pubrinfo->publication;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == relpubinfo)

I am unsure if that variable 'relpubinfo' is of much use; it is only
used one time.

Removed relpubinfo

~~~

12. src/bin/pg_dump/t/002_pg_dump.pl

I think there should be more test cases here:

E.g.1. EXCEPT TABLE should also test a list of tables

E.g.2. EXCEPT with optional TABLE keyword ommitted

Added a test for list of tables and modified one of the test to remove TABLE.

~~~

13. src/bin/psql/describe.c - question about the SQL

Since the new 'except' is a boolean column, wouldn't it be more
natural if all the SQL was treating it as one?

e.g. should the SQL be saying "IS preexpect", "IS NOT prexcept";
instead of comparing preexpect to 't' and 'f' character.

modified

~~~

14. .../t/032_rep_changes_except_table.pl

+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# option.
+# Create schemas and tables on publisher

"option" -> "clause"

Modified.

Thanks for the comments. The v8 patch attached at [1]/messages/by-id/CALDaNm0sAU4s1KTLOEWv=rYo5dQK6uFTJn_0FKj3XG1Nv4D-qw@mail.gmail.com has the fixes
for the same.
[1]: /messages/by-id/CALDaNm0sAU4s1KTLOEWv=rYo5dQK6uFTJn_0FKj3XG1Nv4D-qw@mail.gmail.com

Regards,
Vignesh

#61Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#58)
Re: Skipping schema changes in publication

On Fri, Jun 3, 2022 at 3:37 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v8 patch has the changes for the same.

AFAICS, the summary of this proposal is that we want to support
exclude of certain objects from publication with two kinds of
variants. The first variant is to add support to exclude specific
tables from ALL TABLES PUBLICATION. Without this feature, users need
to manually add all tables for a database even when she wants to avoid
only a handful of tables from the database say because they contain
sensitive information or are not required. We have seen that other
database like MySQL also provides similar feature [1]https://dev.mysql.com/doc/refman/5.7/en/change-replication-filter.html (See
REPLICATE_WILD_IGNORE_TABLE). The proposed syntax for this is as
follows:

CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

This will allow us to publish all the tables in the current database
except t1 and t2. Now, I see that pg_dump has a similar option
provided by switch --exclude-table but that allows tables matching
patterns which is not the case here. I am not sure if we need a
similar variant here.

Then users will be allowed to reset the publication by:
ALTER PUBLICATION pub1 RESET;

This will reset the publication to the default state which includes
resetting the publication parameters, setting the ALL TABLES flag to
false, and dropping the relations and schemas that are associated with
the publication. I don't know if we want to go further with allowing
to RESET specific parameters and if so which parameters and what would
its syntax be?

The second variant is to add support to exclude certain columns of a
table while publishing a particular table. Currently, users need to
list all required columns' names even if they don't want to hide most
of the columns in the table (for example Create Publication pub For
Table t1 (c1, c2)). Consider user doesn't want to publish the 'salary'
or other sensitive information of executives/employees but would like
to publish all other columns. I feel in such cases it will be a lot of
work for the user especially when the table has many columns. I see
that Oracle has a similar feature [2]https://docs.oracle.com/en/cloud/paas/goldengate-cloud/gwuad/selecting-columns.html#GUID-9A851C8B-48F7-43DF-8D98-D086BE069E20. I think without this it will be
difficult for users to use this feature in some cases. The patch for
this is not proposed but I would imagine syntax for it to be something
like "Create Publication pub For Table t1 Except (c3)" and similar
variants for Alter Publication.

Have I missed anything?

Thoughts on the proposal/syntax would be appreciated?

[1]: https://dev.mysql.com/doc/refman/5.7/en/change-replication-filter.html
[2]: https://docs.oracle.com/en/cloud/paas/goldengate-cloud/gwuad/selecting-columns.html#GUID-9A851C8B-48F7-43DF-8D98-D086BE069E20

--
With Regards,
Amit Kapila.

#62houzj.fnst@fujitsu.com
houzj.fnst@fujitsu.com
In reply to: Amit Kapila (#61)
RE: Skipping schema changes in publication

On Wednesday, June 8, 2022 7:04 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Fri, Jun 3, 2022 at 3:37 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v8 patch has the changes for the

same.

AFAICS, the summary of this proposal is that we want to support
exclude of certain objects from publication with two kinds of
variants. The first variant is to add support to exclude specific
tables from ALL TABLES PUBLICATION. Without this feature, users need
to manually add all tables for a database even when she wants to avoid
only a handful of tables from the database say because they contain
sensitive information or are not required. We have seen that other
database like MySQL also provides similar feature [1] (See
REPLICATE_WILD_IGNORE_TABLE). The proposed syntax for this is as
follows:

CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

This will allow us to publish all the tables in the current database
except t1 and t2. Now, I see that pg_dump has a similar option
provided by switch --exclude-table but that allows tables matching
patterns which is not the case here. I am not sure if we need a
similar variant here.

Then users will be allowed to reset the publication by:
ALTER PUBLICATION pub1 RESET;

This will reset the publication to the default state which includes
resetting the publication parameters, setting the ALL TABLES flag to
false, and dropping the relations and schemas that are associated with
the publication. I don't know if we want to go further with allowing
to RESET specific parameters and if so which parameters and what would
its syntax be?

The second variant is to add support to exclude certain columns of a
table while publishing a particular table. Currently, users need to
list all required columns' names even if they don't want to hide most
of the columns in the table (for example Create Publication pub For
Table t1 (c1, c2)). Consider user doesn't want to publish the 'salary'
or other sensitive information of executives/employees but would like
to publish all other columns. I feel in such cases it will be a lot of
work for the user especially when the table has many columns. I see
that Oracle has a similar feature [2]. I think without this it will be
difficult for users to use this feature in some cases. The patch for
this is not proposed but I would imagine syntax for it to be something
like "Create Publication pub For Table t1 Except (c3)" and similar
variants for Alter Publication.

I think the feature to exclude certain columns of a table would be useful.

In some production scenarios, we usually do not want to replicate
sensitive fields(column) in the table. Although we already can achieve
this by specify all replicated columns in the list[1]CREATE TABLE test(a int, b int, c int,..., sensitive text); CRAETE PUBLICATION pub FOR TABLE test(a,b,c,...);, but that seems a
hard work when the table has hundreds of columns.

[1]: CREATE TABLE test(a int, b int, c int,..., sensitive text); CRAETE PUBLICATION pub FOR TABLE test(a,b,c,...);
CREATE TABLE test(a int, b int, c int,..., sensitive text);
CRAETE PUBLICATION pub FOR TABLE test(a,b,c,...);

In addition, it's not easy to maintain the column list like above. Because
we sometimes need to add new fields or delete fields due to business
needs. Every time we add a column(or delete a column in column list), we
need to update the column list.

If we support Except:
CRAETE PUBLICATION pub FOR TABLE test EXCEPT (sensitive);

We don't need to update the column list in most cases.

Thanks for "hametan" for providing the use case off-list.

Best regards,
Hou zj

#63Amit Kapila
amit.kapila16@gmail.com
In reply to: houzj.fnst@fujitsu.com (#62)
Re: Skipping schema changes in publication

On Tue, Jun 14, 2022 at 9:10 AM houzj.fnst@fujitsu.com
<houzj.fnst@fujitsu.com> wrote:

On Wednesday, June 8, 2022 7:04 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Fri, Jun 3, 2022 at 3:37 PM vignesh C <vignesh21@gmail.com> wrote:

Thanks for the comments, the attached v8 patch has the changes for the

same.

AFAICS, the summary of this proposal is that we want to support
exclude of certain objects from publication with two kinds of
variants. The first variant is to add support to exclude specific
tables from ALL TABLES PUBLICATION. Without this feature, users need
to manually add all tables for a database even when she wants to avoid
only a handful of tables from the database say because they contain
sensitive information or are not required. We have seen that other
database like MySQL also provides similar feature [1] (See
REPLICATE_WILD_IGNORE_TABLE). The proposed syntax for this is as
follows:

CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

This will allow us to publish all the tables in the current database
except t1 and t2. Now, I see that pg_dump has a similar option
provided by switch --exclude-table but that allows tables matching
patterns which is not the case here. I am not sure if we need a
similar variant here.

Then users will be allowed to reset the publication by:
ALTER PUBLICATION pub1 RESET;

This will reset the publication to the default state which includes
resetting the publication parameters, setting the ALL TABLES flag to
false, and dropping the relations and schemas that are associated with
the publication. I don't know if we want to go further with allowing
to RESET specific parameters and if so which parameters and what would
its syntax be?

The second variant is to add support to exclude certain columns of a
table while publishing a particular table. Currently, users need to
list all required columns' names even if they don't want to hide most
of the columns in the table (for example Create Publication pub For
Table t1 (c1, c2)). Consider user doesn't want to publish the 'salary'
or other sensitive information of executives/employees but would like
to publish all other columns. I feel in such cases it will be a lot of
work for the user especially when the table has many columns. I see
that Oracle has a similar feature [2]. I think without this it will be
difficult for users to use this feature in some cases. The patch for
this is not proposed but I would imagine syntax for it to be something
like "Create Publication pub For Table t1 Except (c3)" and similar
variants for Alter Publication.

I think the feature to exclude certain columns of a table would be useful.

In some production scenarios, we usually do not want to replicate
sensitive fields(column) in the table. Although we already can achieve
this by specify all replicated columns in the list[1], but that seems a
hard work when the table has hundreds of columns.

[1]
CREATE TABLE test(a int, b int, c int,..., sensitive text);
CRAETE PUBLICATION pub FOR TABLE test(a,b,c,...);

In addition, it's not easy to maintain the column list like above. Because
we sometimes need to add new fields or delete fields due to business
needs. Every time we add a column(or delete a column in column list), we
need to update the column list.

If we support Except:
CRAETE PUBLICATION pub FOR TABLE test EXCEPT (sensitive);

We don't need to update the column list in most cases.

Right, this is a valid point and I think it makes sense for me to
support such a feature for column list and also to exclude a
particular table(s) from the ALL TABLES publication.

Peter E., Euler, and others, do you have any objections to supporting
the above-mentioned two cases?

--
With Regards,
Amit Kapila.

#64vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#58)
2 attachment(s)
Re: Skipping schema changes in publication

On Fri, Jun 3, 2022 at 3:36 PM vignesh C <vignesh21@gmail.com> wrote:

On Thu, May 26, 2022 at 7:04 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

CacheInvalidateRelcacheAll should be called if we change all tables
from true to false, else the cache will not be invalidated. Modified

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

Added errdetail and split it

(5) typo

<varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

Modified

(6) Minor suggestion for an expression change

Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

I felt the existing is better.

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

Modified

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Changed it to "Publication is having default publication parameter values"

Thanks for the comments, the attached v8 patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of commit
"0c20dd33db1607d6a85ffce24238c1e55e384b49", attached a rebased v8
version for the changes of the same.

Regards,
Vignesh

Attachments:

v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchapplication/x-patch; name=v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 691763bbd83df6e5a372fc979526b8876486bd1a Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v8 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  38 ++++++--
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 292 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 3e338f4cc5..d4c23debd1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -217,6 +231,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 89a005540f..49e45114aa 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1509,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..9c51a4db8b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10566,6 +10566,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10612,6 +10614,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index f265e043e9..9ceae3a470 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1817,7 +1817,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b376031856..f7b4742b76 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4053,7 +4053,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 428c1f16c7..097efa708f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..72765994dd 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchapplication/x-patch; name=v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 75d1f2f39010f7bea99fb7b9527edab10d2be4c3 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Mon, 8 Aug 2022 12:17:51 +0530
Subject: [PATCH v8 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 199 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 .../t/032_rep_changes_except_table.pl         |  80 +++++++
 24 files changed, 688 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cd2cc37aeb..9d14db5607 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6436,6 +6436,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index bdf1e7b727..b17e35049f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1377,10 +1377,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d4c23debd1..0ee2aa27a5 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -224,6 +231,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 5790d76270..da4511d5af 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -357,6 +373,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7ba6e4efcb..0bf545149e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1871,8 +1871,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 6af3570005..bac2e217b7 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1058,7 +1079,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 49e45114aa..0b71b7707c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+											  PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1160,6 +1165,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1198,6 +1224,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1507,6 +1606,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1752,6 +1865,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1824,6 +1938,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1899,8 +2014,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 70b94bbb39..f923bfbb4b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16268,7 +16268,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16405,7 +16405,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9c51a4db8b..c446f134a3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -459,7 +459,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -593,6 +593,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10418,7 +10419,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10438,12 +10439,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10481,6 +10483,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10556,6 +10559,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10568,6 +10590,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10594,6 +10618,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a3c1ba8a40..be86d4beb7 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2011,7 +2011,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2100,22 +2101,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2134,7 +2119,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2149,6 +2135,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2224,6 +2212,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 00dc0f2403..7263ef4cfa 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5611,6 +5611,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5644,7 +5646,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5658,14 +5660,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5694,7 +5701,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5711,7 +5718,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da6605175a..af1b3ab657 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4009,8 +4011,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool		first = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4180,6 +4208,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4191,8 +4220,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4203,6 +4241,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4218,6 +4257,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4229,6 +4269,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4249,7 +4290,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4288,6 +4333,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9975,6 +10023,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17900,6 +17951,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 69ee939d44..a2a1ecc8ed 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index b10e1c4c0d..9c4c53a8c3 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 327a69487b..7b24380edc 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2983,17 +2983,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6417,8 +6436,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6436,6 +6460,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 9ceae3a470..f00299683e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1820,9 +1820,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2982,7 +2986,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index c298327f5e..f95012bf08 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7b4742b76..9f6edfebad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4017,6 +4017,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4025,6 +4026,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 097efa708f..49ebb8eef3 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +214,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1656,9 +1697,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,7 +1722,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1694,6 +1758,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1712,6 +1781,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1729,6 +1804,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1750,9 +1831,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; 
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 72765994dd..e37284eb7c 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1060,17 +1073,30 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1078,6 +1104,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1085,6 +1114,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1092,6 +1125,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1101,10 +1138,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..175e38342e
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#65vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#64)
2 attachment(s)
Re: Skipping schema changes in publication

On Mon, Aug 8, 2022 at 12:46 PM vignesh C <vignesh21@gmail.com> wrote:

On Fri, Jun 3, 2022 at 3:36 PM vignesh C <vignesh21@gmail.com> wrote:

On Thu, May 26, 2022 at 7:04 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

CacheInvalidateRelcacheAll should be called if we change all tables
from true to false, else the cache will not be invalidated. Modified

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

Added errdetail and split it

(5) typo

<varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

Modified

(6) Minor suggestion for an expression change

Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

I felt the existing is better.

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

Modified

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Changed it to "Publication is having default publication parameter values"

Thanks for the comments, the attached v8 patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of commit
"0c20dd33db1607d6a85ffce24238c1e55e384b49", attached a rebased v8
version for the changes of the same.

I had missed attaching one of the changes that was present locally.
The updated patch has the changes for the same.

Regards,
Vignesh

Attachments:

v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 65cff42fd81d6c609f96a23c088c8cef83947bb5 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v8 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  38 ++++++--
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 292 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 3e338f4cc5..d4c23debd1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -217,6 +231,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 89a005540f..49e45114aa 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1509,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..9c51a4db8b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10566,6 +10566,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10612,6 +10614,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index f265e043e9..9ceae3a470 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1817,7 +1817,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b376031856..f7b4742b76 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4053,7 +4053,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 428c1f16c7..097efa708f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1653,6 +1653,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9eb86fd54f..72765994dd 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1057,6 +1057,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From dd372848e4b1b87f55efe326cbe75be4c128d22f Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Mon, 8 Aug 2022 12:17:51 +0530
Subject: [PATCH v8 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 199 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 .../t/032_rep_changes_except_table.pl         |  80 +++++++
 24 files changed, 688 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cd2cc37aeb..9d14db5607 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6436,6 +6436,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index bdf1e7b727..b17e35049f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1377,10 +1377,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d4c23debd1..0ee2aa27a5 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -224,6 +231,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 5790d76270..da4511d5af 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -357,6 +373,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7ba6e4efcb..0bf545149e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1871,8 +1871,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 6af3570005..bac2e217b7 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1058,7 +1079,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 49e45114aa..0b71b7707c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (list_length(schemaidlist) > 0 && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (list_length(schemaidlist) > 0 && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (list_length(relations) > 0)
-		{
-			List	   *rels;
+	if (list_length(relations) > 0)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+											  PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (list_length(schemaidlist) > 0)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (list_length(schemaidlist) > 0)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1160,6 +1165,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1198,6 +1224,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1507,6 +1606,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1752,6 +1865,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1824,6 +1938,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1899,8 +2014,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 70b94bbb39..f923bfbb4b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16268,7 +16268,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		list_length(GetRelationPublications(RelationGetRelid(rel))) > 0)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16405,7 +16405,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9c51a4db8b..c446f134a3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -459,7 +459,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -593,6 +593,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10418,7 +10419,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10438,12 +10439,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10481,6 +10483,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10556,6 +10559,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10568,6 +10590,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10594,6 +10618,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a3c1ba8a40..be86d4beb7 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2011,7 +2011,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2100,22 +2101,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2134,7 +2119,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2149,6 +2135,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2224,6 +2212,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 00dc0f2403..7263ef4cfa 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5611,6 +5611,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5644,7 +5646,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5658,14 +5660,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5694,7 +5701,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5711,7 +5718,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da6605175a..af1b3ab657 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4009,8 +4011,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool		first = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4180,6 +4208,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4191,8 +4220,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4203,6 +4241,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4218,6 +4257,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4229,6 +4269,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4249,7 +4290,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4288,6 +4333,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9975,6 +10023,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17900,6 +17951,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 69ee939d44..a2a1ecc8ed 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index b10e1c4c0d..9c4c53a8c3 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 327a69487b..7b24380edc 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2983,17 +2983,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6417,8 +6436,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6436,6 +6460,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 9ceae3a470..f00299683e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1820,9 +1820,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2982,7 +2986,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index c298327f5e..f95012bf08 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7b4742b76..9f6edfebad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4017,6 +4017,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4025,6 +4026,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 097efa708f..dc021f2346 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +214,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1656,9 +1697,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1675,7 +1722,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1694,6 +1758,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1712,6 +1781,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1729,6 +1804,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1750,9 +1831,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 72765994dd..e37284eb7c 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1060,17 +1073,30 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1078,6 +1104,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1085,6 +1114,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1092,6 +1125,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1101,10 +1138,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..175e38342e
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#66Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: vignesh C (#65)
Re: Skipping schema changes in publication

I spent some time on understanding the proposal and the patch. Here
are a few comments wrt the test cases.

+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset

The results for the above two cases are the same before and after the
reset. Is there any way to verify that?
---

+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+

I did not understand the objective of these tests. I think we need to
improve the comments.

Thanks & Regards,

Show quoted text

On Mon, Aug 8, 2022 at 2:53 PM vignesh C <vignesh21@gmail.com> wrote:

On Mon, Aug 8, 2022 at 12:46 PM vignesh C <vignesh21@gmail.com> wrote:

On Fri, Jun 3, 2022 at 3:36 PM vignesh C <vignesh21@gmail.com> wrote:

On Thu, May 26, 2022 at 7:04 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

CacheInvalidateRelcacheAll should be called if we change all tables
from true to false, else the cache will not be invalidated. Modified

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

Added errdetail and split it

(5) typo

<varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

Modified

(6) Minor suggestion for an expression change

Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

I felt the existing is better.

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

Modified

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Changed it to "Publication is having default publication parameter values"

Thanks for the comments, the attached v8 patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of commit
"0c20dd33db1607d6a85ffce24238c1e55e384b49", attached a rebased v8
version for the changes of the same.

I had missed attaching one of the changes that was present locally.
The updated patch has the changes for the same.

Regards,
Vignesh

#67vignesh C
vignesh21@gmail.com
In reply to: Nitin Jadhav (#66)
Re: Skipping schema changes in publication

On Thu, Aug 18, 2022 at 12:33 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

I spent some time on understanding the proposal and the patch. Here
are a few comments wrt the test cases.

+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset

The results for the above two cases are the same before and after the
reset. Is there any way to verify that?

If you see the expected, first \dRp+ command includes:
+Tables:
+ "pub_sch1.tbl1"
The second \dRp+ does not include the Tables.
We are trying to verify that after reset, the tables will be removed
from the publication.
The second test is similar to the first, the only difference here is
that we test schema instead of tables. i.e we verify that the schemas
will be removed from the publication.

---

+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+

I did not understand the objective of these tests. I think we need to
improve the comments.

There are different publications like "ALL TABLES", "TABLE", "ALL
TABLES IN SCHEMA" publications. Here we are trying to verify that
except tables cannot be added to "ALL TABLES", "TABLE", "ALL TABLES IN
SCHEMA" publications.
If you see the expected file, you will see the following error:
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default
publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas
should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication

I felt the existing comment is ok. Let me know if you still feel any
change is required.

Regards,
Vignesh

#68vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#65)
2 attachment(s)
Re: Skipping schema changes in publication

On Mon, Aug 8, 2022 at 2:53 PM vignesh C <vignesh21@gmail.com> wrote:

On Mon, Aug 8, 2022 at 12:46 PM vignesh C <vignesh21@gmail.com> wrote:

On Fri, Jun 3, 2022 at 3:36 PM vignesh C <vignesh21@gmail.com> wrote:

On Thu, May 26, 2022 at 7:04 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

CacheInvalidateRelcacheAll should be called if we change all tables
from true to false, else the cache will not be invalidated. Modified

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

Added errdetail and split it

(5) typo

<varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

Modified

(6) Minor suggestion for an expression change

Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

I felt the existing is better.

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

Modified

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Changed it to "Publication is having default publication parameter values"

Thanks for the comments, the attached v8 patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of commit
"0c20dd33db1607d6a85ffce24238c1e55e384b49", attached a rebased v8
version for the changes of the same.

I had missed attaching one of the changes that was present locally.
The updated patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of a recent
commit. The updated v8 patch has the changes for the same.

Regards,
Vignesh

Attachments:

v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From e567e8c196961c9d65f61ae6e6f05a0e200bb64b Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 14 May 2022 13:13:46 +0530
Subject: [PATCH v8 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  38 ++++++--
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 292 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 3e338f4cc5..d4c23debd1 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -65,20 +66,33 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal> and
-   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.  To alter the owner, you must also be a
-   direct or indirect member of the new owning role. The new owner must have
-   <literal>CREATE</literal> privilege on the database.  Also, the new owner
-   of a <literal>FOR ALL TABLES</literal> or <literal>FOR ALL TABLES IN
-   SCHEMA</literal> publication must be a superuser. However, a superuser can
-   change the ownership of a publication regardless of these restrictions.
+   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must also be a direct or indirect member
+   of the new owning role. The new owner must have <literal>CREATE</literal>
+   privilege on the database.  Also, the new owner of a
+   <literal>FOR ALL TABLES</literal> or
+   <literal>FOR ALL TABLES IN SCHEMA</literal> publication must be a superuser.
+   However, a superuser can change the ownership of a publication regardless of
+   these restrictions.
   </para>
 
   <para>
@@ -217,6 +231,12 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, ALL TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 8b574b86c4..23679d0275 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -53,6 +53,14 @@
 #include "utils/syscache.h"
 #include "utils/varlena.h"
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,11 +99,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1105,6 +1113,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1416,6 +1509,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..9c51a4db8b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10566,6 +10566,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10612,6 +10614,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62a39779b9..28eea9740c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1817,7 +1817,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b376031856..f7b4742b76 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4053,7 +4053,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e6e082de2f..9a682f09b3 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1655,6 +1655,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index a56387edee..e9fcfe703d 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1059,6 +1059,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.32.0

v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v8-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 4f32111c71b83f197ed909c69dd8bb7b2f736ce7 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Thu, 18 Aug 2022 16:15:17 +0530
Subject: [PATCH v8 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 199 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 .../t/032_rep_changes_except_table.pl         |  80 +++++++
 24 files changed, 688 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ff0ed65772..db1e7c8c5e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6436,6 +6436,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index bdf1e7b727..b17e35049f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1377,10 +1377,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d4c23debd1..0ee2aa27a5 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -82,8 +88,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET ALL TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must also be a direct or indirect member
@@ -224,6 +231,14 @@ ALTER PUBLICATION sales_publication ADD ALL TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 5790d76270..da4511d5af 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     ALL TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -120,7 +124,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -156,6 +162,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -357,6 +373,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, ALL TABL
 CREATE PUBLICATION sales_publication FOR ALL TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dd559d62d2..f668788ccd 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1871,8 +1871,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 6af3570005..bac2e217b7 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1058,7 +1079,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 23679d0275..7ffcf76a3c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -193,6 +193,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -305,7 +310,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -332,7 +337,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -381,7 +387,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -400,7 +406,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -844,54 +851,52 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR ALL TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
+	/* FOR ALL TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR ALL TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if ((relations != NIL))
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
-												  PUBLICATIONOBJ_TABLE);
+		rels = OpenTableList(relations);
+		CheckObjSchemaNotAlreadyInPublication(rels, schemaidlist,
+											  PUBLICATIONOBJ_TABLE);
 
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(rels, pstate->p_sourcetext,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(rels, pstate->p_sourcetext,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1160,6 +1165,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1198,6 +1224,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1507,6 +1606,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1752,6 +1865,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1824,6 +1938,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1899,8 +2014,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8d7c68b8b3..70f269f4fe 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16268,7 +16268,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
@@ -16405,7 +16405,7 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
 	{
 		ListCell   *lc;
 		List	   *schemaPubids = GetSchemaPublications(nspOid);
-		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel));
+		List	   *relPubids = GetRelationPublications(RelationGetRelid(rel), false);
 
 		foreach(lc, relPubids)
 		{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9c51a4db8b..c446f134a3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -459,7 +459,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -593,6 +593,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10418,7 +10419,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10438,12 +10439,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10481,6 +10483,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| ALL TABLES IN_P SCHEMA ColId
 				{
@@ -10556,6 +10559,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10568,6 +10590,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10594,6 +10618,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 62e0ffecd8..353bc46f8a 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2011,7 +2011,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2100,22 +2101,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2134,7 +2119,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2149,6 +2135,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2224,6 +2212,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 00dc0f2403..7263ef4cfa 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5611,6 +5611,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5644,7 +5646,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5658,14 +5660,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5694,7 +5701,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5711,7 +5718,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da6605175a..af1b3ab657 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -127,6 +127,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4009,8 +4011,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+		bool		first = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4180,6 +4208,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4191,8 +4220,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4203,6 +4241,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4218,6 +4257,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4229,6 +4269,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4249,7 +4290,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4288,6 +4333,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9975,6 +10023,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17900,6 +17951,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 69ee939d44..a2a1ecc8ed 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index b10e1c4c0d..9c4c53a8c3 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2454,6 +2454,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 327a69487b..7b24380edc 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2983,17 +2983,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6417,8 +6436,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6436,6 +6460,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 28eea9740c..01e8e58b5f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1820,9 +1820,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("ALL TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES IN SCHEMA", "ALL TABLES", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "IN SCHEMA");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -2987,7 +2991,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "TABLES IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("IN SCHEMA", "WITH (");
+		COMPLETE_WITH("IN SCHEMA", "WITH (", "EXCEPT TABLE");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index c298327f5e..f95012bf08 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 57df3fc1e3..e4e4ed17ab 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7b4742b76..9f6edfebad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4017,6 +4017,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4025,6 +4026,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 9a682f09b3..fdcc74486b 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -165,13 +165,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -190,8 +214,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1658,9 +1699,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1677,7 +1724,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1696,6 +1760,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1714,6 +1783,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1731,6 +1806,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1752,9 +1833,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index e9fcfe703d..f3fcd865ae 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -89,20 +89,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1062,17 +1075,30 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1080,6 +1106,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1087,6 +1116,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1094,6 +1127,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1103,10 +1140,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..175e38342e
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.32.0

#69Ian Lawrence Barwick
barwick@gmail.com
In reply to: vignesh C (#68)
Re: Skipping schema changes in publication

2022年8月19日(金) 2:41 vignesh C <vignesh21@gmail.com>:

On Mon, Aug 8, 2022 at 2:53 PM vignesh C <vignesh21@gmail.com> wrote:

On Mon, Aug 8, 2022 at 12:46 PM vignesh C <vignesh21@gmail.com> wrote:

On Fri, Jun 3, 2022 at 3:36 PM vignesh C <vignesh21@gmail.com> wrote:

On Thu, May 26, 2022 at 7:04 PM osumi.takamichi@fujitsu.com
<osumi.takamichi@fujitsu.com> wrote:

On Monday, May 23, 2022 2:13 PM vignesh C <vignesh21@gmail.com> wrote:

Attached v7 patch which fixes the buildfarm warning for an unused warning in
release mode as in [1].

Hi, thank you for the patches.

I'll share several review comments.

For v7-0001.

(1) I'll suggest some minor rewording.

+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication options, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.

My suggestion is
"The RESET clause will reset the publication to the
default state. It resets the publication operations,
sets ALL TABLES flag to false and drops all relations
and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(2) typo and rewording

+/*
+ * Reset the publication.
+ *
+ * Reset the publication options, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */

The "setting" in this sentence should be "set".

How about changing like below ?
FROM:
"Reset the publication options, setting ALL TABLES flag to false and drop
all relations and schemas that are associated with the publication."
TO:
"Reset the publication operations, set ALL TABLES flag to false and drop
all relations and schemas associated with the publication."

I felt the existing looks better. I would prefer to keep it that way.

(3) AlterPublicationReset

Do we need to call CacheInvalidateRelcacheAll() or
InvalidatePublicationRels() at the end of
AlterPublicationReset() like AlterPublicationOptions() ?

CacheInvalidateRelcacheAll should be called if we change all tables
from true to false, else the cache will not be invalidated. Modified

For v7-0002.

(4)

+       if (stmt->for_all_tables)
+       {
+               bool            isdefault = CheckPublicationDefValues(tup);
+
+               if (!isdefault)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                       errmsg("adding ALL TABLES requires the publication to have default publication options, no tables/....
+                                       errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));

The errmsg string has three messages for user and is a bit long
(we have two sentences there connected by 'and').
Can't we make it concise and split it into a couple of lines for code readability ?

I'll suggest a change below.
FROM:
"adding ALL TABLES requires the publication to have default publication options, no tables/schemas associated and ALL TABLES flag should not be set"
TO:
"adding ALL TABLES requires the publication defined not for ALL TABLES"
"to have default publish actions without any associated tables/schemas"

Added errdetail and split it

(5) typo

<varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to exclude from the publication.
+      It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+

Kindly change
FROM:
This clause specifies a list of tables to exclude from the publication.
TO:
This clause specifies a list of tables to be excluded from the publication.
or
This clause specifies a list of tables excluded from the publication.

Modified

(6) Minor suggestion for an expression change

Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.

I'll suggest a minor rewording.
FROM:
...exclude replicating the changes for the specified tables
TO:
...exclude replication changes for the specified tables

I felt the existing is better.

(7)
(7-1)

+/*
+ * Check if the publication has default values
+ *
+ * Check the following:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default options
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

I think this header comment can be improved.
FROM:
Check the following:
TO:
Returns true if the publication satisfies all the following conditions:

Modified

(7-2)

b) should be changed as well
FROM:
Publication is having default options
TO:
Publication has the default publish operations

Changed it to "Publication is having default publication parameter values"

Thanks for the comments, the attached v8 patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of commit
"0c20dd33db1607d6a85ffce24238c1e55e384b49", attached a rebased v8
version for the changes of the same.

I had missed attaching one of the changes that was present locally.
The updated patch has the changes for the same.

The patch needed to be rebased on top of HEAD because of a recent
commit. The updated v8 patch has the changes for the same.

Hi

cfbot reports the patch no longer applies [1]http://cfbot.cputube.org/patch_40_3646.log. As CommitFest 2022-11 is
currently underway, this would be an excellent time to update the patch.

[1]: http://cfbot.cputube.org/patch_40_3646.log

Thanks

Ian Barwick

#70vignesh C
vignesh21@gmail.com
In reply to: Ian Lawrence Barwick (#69)
2 attachment(s)
Re: Skipping schema changes in publication

On Fri, 4 Nov 2022 at 08:19, Ian Lawrence Barwick <barwick@gmail.com> wrote:

Hi

cfbot reports the patch no longer applies [1]. As CommitFest 2022-11 is
currently underway, this would be an excellent time to update the patch.

[1] http://cfbot.cputube.org/patch_40_3646.log

Here is an updated patch which is rebased on top of HEAD.

Regards,
Vignesh

Attachments:

v9-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v9-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 22d17bd8eeb2c35870b80cbc41c23e89e2c8f981 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Sun, 6 Nov 2022 06:34:44 +0530
Subject: [PATCH v9 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  25 +++++-
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 285 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index c84b11f47a..2f0509ec43 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -67,14 +68,26 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
    <literal>CREATE</literal> privilege on the database.  Also, the new owner
@@ -210,6 +223,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a8b75eb1be..3cff1635bf 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -54,6 +54,14 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -92,11 +100,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1078,6 +1086,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1400,6 +1493,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index deb101710e..1c042eb1a7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10482,6 +10482,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10528,6 +10530,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4c45e4747a..4c4664b284 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1817,7 +1817,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7caff62af7..47a1757e95 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3768,7 +3768,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 427f87ea07..708cdbbd76 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1723,6 +1723,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index a47c5939d5..ec710ff33d 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1093,6 +1093,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.34.1

v9-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v9-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From da87b79269b1dfcc847836dd54c03b489f00de6c Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Mon, 7 Nov 2022 12:20:28 +0530
Subject: [PATCH v9 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  91 +++++++-
 src/test/regress/sql/publication.sql          |  48 ++++-
 .../t/032_rep_changes_except_table.pl         |  80 +++++++
 24 files changed, 689 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 00f833d210..c86e30182e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6455,6 +6455,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f8756389a3..16e719b131 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1721,10 +1721,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 2f0509ec43..2a629188af 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -84,8 +90,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
@@ -216,6 +223,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index e229384e6f..4c467c2563 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -122,7 +126,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -158,6 +164,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -372,6 +388,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 9494f28063..c83f487487 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1871,8 +1871,9 @@ testdb=&gt;
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 59967098b3..9ba84045c8 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1058,7 +1079,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 3cff1635bf..fd380542e0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -196,6 +196,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -270,7 +275,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -297,7 +302,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -346,7 +352,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -365,7 +371,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -821,52 +828,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+									schemaidlist != NIL,
+									publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1133,6 +1138,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1171,6 +1197,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1491,6 +1590,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1703,6 +1816,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1775,6 +1889,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1850,8 +1965,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6007e10730..eaae63a8ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16313,7 +16313,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1c042eb1a7..6c75f6ff4e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -452,7 +452,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -586,6 +586,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10334,7 +10335,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10354,12 +10355,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10397,6 +10399,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10472,6 +10475,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10484,6 +10506,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10510,6 +10534,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 2ecaa5b907..4091dfd467 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2007,7 +2007,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2096,22 +2097,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2130,7 +2115,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2145,6 +2131,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2220,6 +2208,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index bd6cd4e47b..7a60cb5970 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5611,6 +5611,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5644,7 +5646,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5658,14 +5660,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5694,7 +5701,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5711,7 +5718,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da427f4d4a..26f46bdd44 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -128,6 +128,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4010,8 +4012,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4181,6 +4209,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4192,8 +4221,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4204,6 +4242,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4219,6 +4258,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4230,6 +4270,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4250,7 +4291,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4289,6 +4334,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9976,6 +10024,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17934,6 +17985,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 427f5d45f6..cf1c2990d6 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 8dc1f0eccb..ac945bf5e5 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2466,6 +2466,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c645d66418..3cb7eae4e9 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2983,17 +2983,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6417,8 +6436,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6436,6 +6460,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4c4664b284..1356eaa199 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1820,9 +1820,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -3023,7 +3027,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index ecf5a28e00..8dd62dc7ce 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 2491196570..92fc6e7773 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid subid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 47a1757e95..ddee1251d1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3732,6 +3732,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -3740,6 +3741,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 708cdbbd76..8f1b877d07 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -192,13 +192,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -217,8 +241,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1726,9 +1767,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1745,7 +1792,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1763,7 +1827,12 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1782,6 +1851,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1799,6 +1874,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1820,9 +1901,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index ec710ff33d..5a3fa8f582 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -98,20 +98,34 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1096,23 +1110,39 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1121,6 +1151,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1128,6 +1162,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1137,10 +1175,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..175e38342e
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#71Ian Lawrence Barwick
barwick@gmail.com
In reply to: vignesh C (#70)
Re: Skipping schema changes in publication

2022年11月7日(月) 22:39 vignesh C <vignesh21@gmail.com>:

On Fri, 4 Nov 2022 at 08:19, Ian Lawrence Barwick <barwick@gmail.com> wrote:

Hi

cfbot reports the patch no longer applies [1]. As CommitFest 2022-11 is
currently underway, this would be an excellent time to update the patch.

[1] http://cfbot.cputube.org/patch_40_3646.log

Here is an updated patch which is rebased on top of HEAD.

Thanks for the updated patch.

While reviewing the patch backlog, we have determined that this patch adds
one or more TAP tests but has not added the test to the "meson.build" file.

To do this, locate the relevant "meson.build" file for each test and add it
in the 'tests' dictionary, which will look something like this:

'tap': {
'tests': [
't/001_basic.pl',
],
},

For some additional details please see this Wiki article:

https://wiki.postgresql.org/wiki/Meson_for_patch_authors

For more information on the meson build system for PostgreSQL see:

https://wiki.postgresql.org/wiki/Meson

Regards

Ian Barwick

#72vignesh C
vignesh21@gmail.com
In reply to: Ian Lawrence Barwick (#71)
2 attachment(s)
Re: Skipping schema changes in publication

On Wed, 16 Nov 2022 at 09:34, Ian Lawrence Barwick <barwick@gmail.com> wrote:

2022年11月7日(月) 22:39 vignesh C <vignesh21@gmail.com>:

On Fri, 4 Nov 2022 at 08:19, Ian Lawrence Barwick <barwick@gmail.com> wrote:

Hi

cfbot reports the patch no longer applies [1]. As CommitFest 2022-11 is
currently underway, this would be an excellent time to update the patch.

[1] http://cfbot.cputube.org/patch_40_3646.log

Here is an updated patch which is rebased on top of HEAD.

Thanks for the updated patch.

While reviewing the patch backlog, we have determined that this patch adds
one or more TAP tests but has not added the test to the "meson.build" file.

Thanks, I have updated the meson.build to include the TAP test. The
attached patch has the changes for the same.

Regards,
Vignesh

Attachments:

v9-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchtext/x-patch; charset=US-ASCII; name=v9-0001-Add-RESET-clause-to-Alter-Publication-which-will-.patchDownload
From 14d2d15c207dd73d141f9d4ac6178d6f49e95b91 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Sun, 6 Nov 2022 06:34:44 +0530
Subject: [PATCH v9 1/2] Add RESET clause to Alter Publication which will reset
 the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  25 +++++-
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 285 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index c84b11f47a..2f0509ec43 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -67,14 +68,26 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
    direct or indirect member of the new owning role. The new owner must have
    <literal>CREATE</literal> privilege on the database.  Also, the new owner
@@ -210,6 +223,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 940655b9be..e1f9e8bf93 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -55,6 +55,14 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -93,11 +101,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1079,6 +1087,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1401,6 +1494,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2a910ded15..08fb132199 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10483,6 +10483,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10529,6 +10531,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index a0e26bc295..98c4d960aa 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1818,7 +1818,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7caff62af7..47a1757e95 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3768,7 +3768,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 427f87ea07..708cdbbd76 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1723,6 +1723,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index a47c5939d5..ec710ff33d 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1093,6 +1093,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.34.1

v9-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchtext/x-patch; charset=US-ASCII; name=v9-0002-Skip-publishing-the-tables-specified-in-EXCEPT-TA.patchDownload
From 01bf0c045e0d9ea44e6d3254ab69443e9bdbe3bd Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Mon, 7 Nov 2022 12:20:28 +0530
Subject: [PATCH v9 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  91 +++++++-
 src/test/regress/sql/publication.sql          |  48 ++++-
 src/test/subscription/meson.build             |   1 +
 .../t/032_rep_changes_except_table.pl         |  80 +++++++
 25 files changed, 690 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 00f833d210..c86e30182e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6455,6 +6455,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f8756389a3..16e719b131 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1721,10 +1721,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 2f0509ec43..2a629188af 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -84,8 +90,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the
    invoking user to be a superuser.  To alter the owner, you must also be a
@@ -216,6 +223,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index e229384e6f..4c467c2563 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -122,7 +126,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -158,6 +164,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -372,6 +388,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index d31cf17f5d..08d952c22f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1907,8 +1907,9 @@ INSERT INTO tbl1 VALUES ($1, $2) \bind 'first value' 'second value' \g
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 59967098b3..9ba84045c8 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1058,7 +1079,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index e1f9e8bf93..c5cbb4f365 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -197,6 +197,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -271,7 +276,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -298,7 +303,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -347,7 +353,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -366,7 +372,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -822,52 +829,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+									schemaidlist != NIL,
+									publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1134,6 +1139,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1172,6 +1198,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1492,6 +1591,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1704,6 +1817,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1776,6 +1890,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1851,8 +1966,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f006807852..818dbcf762 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16313,7 +16313,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 08fb132199..58e01e5f1a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -452,7 +452,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -586,6 +586,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10335,7 +10336,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10355,12 +10356,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10398,6 +10400,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10473,6 +10476,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10485,6 +10507,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10511,6 +10535,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 2ecaa5b907..4091dfd467 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2007,7 +2007,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2096,22 +2097,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2130,7 +2115,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2145,6 +2131,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2220,6 +2208,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index bd6cd4e47b..7a60cb5970 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5611,6 +5611,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5644,7 +5646,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5658,14 +5660,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5694,7 +5701,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5711,7 +5718,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da427f4d4a..26f46bdd44 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -128,6 +128,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4010,8 +4012,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4181,6 +4209,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4192,8 +4221,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4204,6 +4242,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4219,6 +4258,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4230,6 +4270,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4250,7 +4291,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4289,6 +4334,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -9976,6 +10024,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -17934,6 +17985,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 427f5d45f6..cf1c2990d6 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb4..a6595f25a6 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 8dc1f0eccb..ac945bf5e5 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2466,6 +2466,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2eae519b1d..fa3149b4ed 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2983,17 +2983,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6419,8 +6438,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6438,6 +6462,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 98c4d960aa..06657cf4c4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1821,9 +1821,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -3024,7 +3028,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index ecf5a28e00..8dd62dc7ce 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index ecd3739f1a..28a33ff17a 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 2491196570..92fc6e7773 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid subid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 47a1757e95..ddee1251d1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3732,6 +3732,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -3740,6 +3741,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 708cdbbd76..8f1b877d07 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -192,13 +192,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -217,8 +241,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1726,9 +1767,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1745,7 +1792,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1763,7 +1827,12 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1782,6 +1851,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1799,6 +1874,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1820,9 +1901,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index ec710ff33d..5a3fa8f582 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -98,20 +98,34 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1096,23 +1110,39 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1121,6 +1151,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1128,6 +1162,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1137,10 +1175,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d1dd9295..b02499490b 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -36,6 +36,7 @@ tests += {
       't/029_on_error.pl',
       't/030_origin.pl',
       't/031_column_list.pl',
+      't/032_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..175e38342e
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#73vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#72)
2 attachment(s)
Re: Skipping schema changes in publication

On Wed, 16 Nov 2022 at 15:35, vignesh C <vignesh21@gmail.com> wrote:

On Wed, 16 Nov 2022 at 09:34, Ian Lawrence Barwick <barwick@gmail.com> wrote:

2022年11月7日(月) 22:39 vignesh C <vignesh21@gmail.com>:

On Fri, 4 Nov 2022 at 08:19, Ian Lawrence Barwick <barwick@gmail.com> wrote:

Hi

cfbot reports the patch no longer applies [1]. As CommitFest 2022-11 is
currently underway, this would be an excellent time to update the patch.

[1] http://cfbot.cputube.org/patch_40_3646.log

Here is an updated patch which is rebased on top of HEAD.

Thanks for the updated patch.

While reviewing the patch backlog, we have determined that this patch adds
one or more TAP tests but has not added the test to the "meson.build" file.

Thanks, I have updated the meson.build to include the TAP test. The
attached patch has the changes for the same.

The patch was not applying on top of HEAD, attached a rebased version.

Regards,
Vignesh

Attachments:

v10-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchtext/x-patch; charset=US-ASCII; name=v10-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 5889eb0b42b6f595873e4a1d3823cc49b5070623 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Thu, 19 Jan 2023 18:13:54 +0530
Subject: [PATCH v10 1/2] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 ++++++--
 src/backend/commands/publicationcmds.c    | 105 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.c               |   2 +-
 src/include/nodes/parsenodes.h            |   3 +-
 src/test/regress/expected/publication.out | 101 +++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  50 +++++++++++
 7 files changed, 291 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index cd20868bca..ab3cebd71c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -67,18 +68,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+   <para>
+    The <literal>RENAME</literal> clause will change the name of the
+    publication.
+   </para>
+
+   <para>
+    The <literal>RESET</literal> clause will reset the publication to the
+    default state which includes resetting the publication parameters, setting
+    <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+    dropping all relations and schemas that are associated with the
+    publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a <literal>FOR ALL TABLES</literal> or
    <literal>FOR TABLES IN SCHEMA</literal>
    publication must be a superuser. However, a superuser can
@@ -212,6 +227,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index f4ba572697..e9ee77ecd3 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -55,6 +55,14 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -93,11 +101,11 @@ parse_publication_options(ParseState *pstate,
 	*publish_via_partition_root_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1079,6 +1087,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1401,6 +1494,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..afbe408fe1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10501,6 +10501,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10547,6 +10549,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..bdfc6d8e88 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1869,7 +1869,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f39ab8586a..772fea0f82 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3789,7 +3789,8 @@ typedef enum AlterPublicationAction
 {
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
-	AP_SetObjects				/* set list of objects */
+	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication			/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 427f87ea07..708cdbbd76 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1723,6 +1723,107 @@ DROP PUBLICATION pub;
 DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index a47c5939d5..ec710ff33d 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1093,6 +1093,56 @@ DROP TABLE sch1.tbl1;
 DROP SCHEMA sch1 cascade;
 DROP SCHEMA sch2 cascade;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
-- 
2.34.1

v10-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchtext/x-patch; charset=US-ASCII; name=v10-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 94f4fc184b0ed06533dad241dafa070295d37855 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Thu, 19 Jan 2023 18:14:47 +0530
Subject: [PATCH v10 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  26 +++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.c                   |  10 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  91 +++++++-
 src/test/regress/sql/publication.sql          |  48 ++++-
 src/test/subscription/meson.build             |   1 +
 .../t/032_rep_changes_except_table.pl         |  80 +++++++
 25 files changed, 690 insertions(+), 129 deletions(-)
 create mode 100644 src/test/subscription/t/032_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1e4048054..c8c13c7a94 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6476,6 +6476,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 1bd5660c87..ad25f43186 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1736,10 +1736,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index ab3cebd71c..f09b3a4902 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -87,8 +93,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -220,6 +227,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index e229384e6f..4c467c2563 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -122,7 +126,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -158,6 +164,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -372,6 +388,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dc6528dc11..4642ab015e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1917,8 +1917,9 @@ INSERT INTO tbl1 VALUES ($1, $2) \bind 'first value' 'second value' \g
         If <replaceable class="parameter">pattern</replaceable> is
         specified, only those publications whose names match the pattern are
         listed.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a98fcad421..496229f357 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -290,7 +290,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -302,32 +303,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table
+			 * is not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -396,6 +409,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -664,9 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -680,7 +695,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -779,13 +795,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -802,7 +821,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -823,7 +843,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1058,7 +1079,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 */
 		if (publication->alltables)
 		{
-			tables = GetAllTablesPublicationRelations(publication->pubviaroot);
+			tables = GetAllTablesPublicationRelations(publication->oid,
+													  publication->pubviaroot);
 		}
 		else
 		{
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index e9ee77ecd3..be1fe66429 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -197,6 +197,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -271,7 +276,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -298,7 +303,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -347,7 +353,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-									bool pubviaroot)
+									bool pubviaroot, bool puballtables)
 {
 	HeapTuple	tuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -366,7 +372,8 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -822,52 +829,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+									publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+									schemaidlist != NIL,
+									publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1134,6 +1139,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
 	replaces[Anum_pg_publication_pubviaroot - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s) to
+	 * the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+						AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk
+	 * of any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+						stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1172,6 +1198,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1492,6 +1591,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated.");
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1704,6 +1817,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1776,6 +1890,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1851,8 +1966,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7c697a285b..7ee53645ce 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16246,7 +16246,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index afbe408fe1..3b0b53df7a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -450,7 +450,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10353,7 +10354,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10373,12 +10374,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10416,6 +10418,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10491,6 +10494,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10503,6 +10525,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10529,6 +10553,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 1a80d67bb9..f48b64b52e 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2051,7 +2051,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2140,22 +2141,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2174,7 +2159,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2189,6 +2175,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2264,6 +2252,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d171cfcf2f..81e7ef9a7d 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5618,6 +5618,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5651,7 +5653,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->cols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5665,14 +5667,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5701,7 +5708,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5718,7 +5725,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_collist_contains_invalid_column(pubid, relation, ancestors,
-												pubform->pubviaroot))
+												pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->cols_valid_for_update = false;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 527c7651ab..29dd0417a8 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -130,6 +130,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4062,8 +4064,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4233,6 +4261,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4244,8 +4273,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 150000 should be changed to 160000 later for PG16. */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4256,6 +4294,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4271,6 +4310,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4282,6 +4322,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4302,7 +4343,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4341,6 +4386,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -10050,6 +10098,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -18007,6 +18058,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index e7cbd8d7ed..ef78d33e7c 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -80,6 +80,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index f963b9a449..b105b67e57 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -90,6 +90,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_REFRESH_MATVIEW,		/* DO_REFRESH_MATVIEW */
 	PRIO_POLICY,				/* DO_POLICY */
 	PRIO_PUBLICATION,			/* DO_PUBLICATION */
+	PRIO_PUBLICATION_EXCEPT_REL,	/* DO_PUBLICATION_EXCEPT_REL */
 	PRIO_PUBLICATION_REL,		/* DO_PUBLICATION_REL */
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,	/* DO_PUBLICATION_TABLE_IN_SCHEMA */
 	PRIO_SUBSCRIPTION			/* DO_SUBSCRIPTION */
@@ -1483,6 +1485,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d92247c915..cd57a3a8a6 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2518,6 +2518,32 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub5' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub5 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql   => 'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+		},
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql   => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..af7590ee2a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2981,17 +2981,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 150000 should be changed to 160000 later for PG16. */
+				if (pset.sversion >= 150000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6420,8 +6439,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6439,6 +6463,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 150000 should be changed to 160000 later for PG16. */
+			if (pset.sversion >= 150000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index bdfc6d8e88..ef78e5e2e8 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1872,9 +1872,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-			 (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") ||
+			 ((HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
+			   HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE")) &&
 			  ends_with(prev_wd, ',')))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
@@ -3096,7 +3100,7 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6ecaa2a01e..bb2f3c2008 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -108,11 +108,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -132,7 +133,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -143,7 +144,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 613e9747c2..9081c4d78f 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 70d5e3680a..6f20826e26 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,8 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid subid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
-												List *ancestors, bool pubviaroot);
+												List *ancestors,
+												bool pubviaroot,
+												bool puballtables);
 
 #endif							/* PUBLICATIONCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 772fea0f82..41cf56fd58 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3753,6 +3753,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -3761,6 +3762,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,		/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 708cdbbd76..8f1b877d07 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -192,13 +192,37 @@ Publications:
  regress_publication_user | t          | t       | t       | f       | f         | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                        Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                       Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                     Publication testpub3
@@ -217,8 +241,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                    Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                    Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1726,9 +1767,15 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1745,7 +1792,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                 Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Via root 
+--------------------------+------------+---------+---------+---------+-----------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1763,7 +1827,12 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1782,6 +1851,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1799,6 +1874,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                  Publication testpub_reset
@@ -1820,9 +1901,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index ec710ff33d..5a3fa8f582 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -98,20 +98,34 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1096,23 +1110,39 @@ DROP SCHEMA sch2 cascade;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1121,6 +1151,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1128,6 +1162,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1137,10 +1175,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 3db0fdfd96..f18235e7c7 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -38,6 +38,7 @@ tests += {
       't/029_on_error.pl',
       't/030_origin.pl',
       't/031_column_list.pl',
+      't/032_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/032_rep_changes_except_table.pl b/src/test/subscription/t/032_rep_changes_except_table.pl
new file mode 100644
index 0000000000..175e38342e
--- /dev/null
+++ b/src/test/subscription/t/032_rep_changes_except_table.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+        "ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1");
+$node_publisher->safe_psql('postgres',
+        "INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#74vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#73)
Re: Skipping schema changes in publication

On Fri, 20 Jan 2023 at 15:30, vignesh C <vignesh21@gmail.com> wrote:

On Wed, 16 Nov 2022 at 15:35, vignesh C <vignesh21@gmail.com> wrote:

On Wed, 16 Nov 2022 at 09:34, Ian Lawrence Barwick <barwick@gmail.com> wrote:

2022年11月7日(月) 22:39 vignesh C <vignesh21@gmail.com>:

On Fri, 4 Nov 2022 at 08:19, Ian Lawrence Barwick <barwick@gmail.com> wrote:

Hi

cfbot reports the patch no longer applies [1]. As CommitFest 2022-11 is
currently underway, this would be an excellent time to update the patch.

[1] http://cfbot.cputube.org/patch_40_3646.log

Here is an updated patch which is rebased on top of HEAD.

Thanks for the updated patch.

While reviewing the patch backlog, we have determined that this patch adds
one or more TAP tests but has not added the test to the "meson.build" file.

Thanks, I have updated the meson.build to include the TAP test. The
attached patch has the changes for the same.

The patch was not applying on top of HEAD, attached a rebased version.

As I did not see much interest from others, I'm withdrawing this patch
for now. But if there is any interest others in future, I would be
more than happy to work on this feature.

Regards,
Vignesh

#75Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#74)
Re: Skipping schema changes in publication

On Tue, Jan 9, 2024 at 12:02 PM vignesh C <vignesh21@gmail.com> wrote:

As I did not see much interest from others, I'm withdrawing this patch
for now. But if there is any interest others in future, I would be
more than happy to work on this feature.

Just FYI, I noticed a use case for this patch in email [1]/messages/by-id/tencent_DCDF626FCD4A556C51BE270FDC3047540208@qq.com. Users
would like to replicate all except a few columns having sensitive
information. The challenge with current column list features is that
adding new tables to columns would lead users to change the respective
publications as well.

[1]: /messages/by-id/tencent_DCDF626FCD4A556C51BE270FDC3047540208@qq.com

--
With Regards,
Amit Kapila.

#76Zhijie Hou (Fujitsu)
houzj.fnst@fujitsu.com
In reply to: Amit Kapila (#75)
RE: Skipping schema changes in publication

On Thu, Apr 10, 2025 at 7:25 PM Amit Kapila wrote:

On Tue, Jan 9, 2024 at 12:02 PM vignesh C <vignesh21@gmail.com> wrote:

As I did not see much interest from others, I'm withdrawing this patch
for now. But if there is any interest others in future, I would be
more than happy to work on this feature.

Just FYI, I noticed a use case for this patch in email [1]. Users would like to
replicate all except a few columns having sensitive information. The challenge
with current column list features is that adding new tables to columns would
lead users to change the respective publications as well.

[1] -
/messages/by-id/tencent_DCDF626FCD4A556C51BE
270FDC3047540208%40qq.com

BTW, I noticed that debezium, an open source distributed platform for change
data capture that replies on logical decoding, also support specifying the
column exclusion list[1]. So, this indicates that there could be some use cases
for this feature.

https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-property-column-exclude-list

Best Regards,
Hou zj

#77Amit Kapila
amit.kapila16@gmail.com
In reply to: Zhijie Hou (Fujitsu) (#76)
Re: Skipping schema changes in publication

On Wed, Apr 16, 2025 at 8:22 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:

On Thu, Apr 10, 2025 at 7:25 PM Amit Kapila wrote:

On Tue, Jan 9, 2024 at 12:02 PM vignesh C <vignesh21@gmail.com> wrote:

As I did not see much interest from others, I'm withdrawing this patch
for now. But if there is any interest others in future, I would be
more than happy to work on this feature.

Just FYI, I noticed a use case for this patch in email [1]. Users would like to
replicate all except a few columns having sensitive information. The challenge
with current column list features is that adding new tables to columns would
lead users to change the respective publications as well.

[1] -
/messages/by-id/tencent_DCDF626FCD4A556C51BE
270FDC3047540208%40qq.com

BTW, I noticed that debezium, an open source distributed platform for change
data capture that replies on logical decoding, also support specifying the
column exclusion list[1]. So, this indicates that there could be some use cases
for this feature.

Thanks for sharing the link. I see that they support both the include
and exclude lists for columns and tables.

--
With Regards,
Amit Kapila.

#78Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Amit Kapila (#77)
2 attachment(s)
Re: Skipping schema changes in publication

On Thu, 17 Apr 2025 at 09:12, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Apr 16, 2025 at 8:22 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:

On Thu, Apr 10, 2025 at 7:25 PM Amit Kapila wrote:

On Tue, Jan 9, 2024 at 12:02 PM vignesh C <vignesh21@gmail.com> wrote:

As I did not see much interest from others, I'm withdrawing this patch
for now. But if there is any interest others in future, I would be
more than happy to work on this feature.

Just FYI, I noticed a use case for this patch in email [1]. Users would like to
replicate all except a few columns having sensitive information. The challenge
with current column list features is that adding new tables to columns would
lead users to change the respective publications as well.

[1] -
/messages/by-id/tencent_DCDF626FCD4A556C51BE
270FDC3047540208%40qq.com

BTW, I noticed that debezium, an open source distributed platform for change
data capture that replies on logical decoding, also support specifying the
column exclusion list[1]. So, this indicates that there could be some use cases
for this feature.

Thanks for sharing the link. I see that they support both the include
and exclude lists for columns and tables.

Hi Hackers,

I see there is some interest in the functionality added by this patch.
I have rebased the patches in [1]/messages/by-id/CALDaNm3dWZCYDih55qTNAYsjCvYXMFv=46UsDWmfCnXMt3kPCg@mail.gmail.com. I saw a new column 'pubgencols' was
added in pg_publication in PG 18. So, I have modified v11-0001 to
RESET this as well.
I am also working on creating a patch to exclude columns in
publication as per suggestion in [2]/messages/by-id/CAA4eK1KRdAPC=5=7tQ1GW0cRwD=zaDMi+T4u_k4GxPhPY6e8BQ@mail.gmail.com.

[1]: /messages/by-id/CALDaNm3dWZCYDih55qTNAYsjCvYXMFv=46UsDWmfCnXMt3kPCg@mail.gmail.com
[2]: /messages/by-id/CAA4eK1KRdAPC=5=7tQ1GW0cRwD=zaDMi+T4u_k4GxPhPY6e8BQ@mail.gmail.com

Thanks and Regards,
Shlok Kyal

Attachments:

v11-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v11-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 714323463d2cb61113646c773efa090425fe716e Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v11 1/2] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 +++++--
 src/backend/commands/publicationcmds.c    | 111 ++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 120 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 ++++++++++
 7 files changed, 321 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..06452af9214 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +245,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..159dc3781d0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,94 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1598,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0b5652071d1..952e8e103cf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10762,6 +10762,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10808,6 +10810,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 620830feb9d..d59ed5f3fd0 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2242,7 +2242,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index dd00ab420b8..7280e9836cf 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4272,6 +4272,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..b2ffe0a8c20 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,126 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
+                                            ^
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..15b2b1cfd28 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v11-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v11-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 57d3fd46882f3b93063ec3171b823886bef9b907 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 19:08:35 +0530
Subject: [PATCH v11 2/2] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  16 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  97 ++++++++-
 src/test/regress/sql/publication.sql          |  47 ++++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++++
 25 files changed, 689 insertions(+), 134 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fa86c569dc4..4e37c928b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 686dd441d02..022c4fd4e2c 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2251,10 +2251,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 06452af9214..37e2c84bc10 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -89,8 +95,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -238,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..be61318a0dc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +458,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 8f7d8758ca0..3cce762fa97 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2094,8 +2094,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..ec580e3b050 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -861,13 +877,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +903,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +925,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1181,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 159dc3781d0..5194b2fb6e2 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1246,6 +1251,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1284,6 +1310,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1596,6 +1695,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1808,6 +1921,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1880,6 +1994,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1955,8 +2070,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d813..8a8268a05d2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8624,7 +8624,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
@@ -18794,7 +18794,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 952e8e103cf..89ac0495ce8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -585,6 +585,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10614,7 +10615,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10634,12 +10635,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10677,6 +10679,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10752,6 +10755,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10764,6 +10786,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10790,6 +10814,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 693a766e6d7..5512b4cba7f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2063,7 +2063,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2174,22 +2175,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2208,7 +2193,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2223,6 +2209,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2301,6 +2289,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..bffdab2ab63 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5845,14 +5847,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5890,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5908,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 37432e66efd..92db5ca8d97 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -183,6 +183,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4510,8 +4512,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4677,6 +4705,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4688,8 +4717,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4700,6 +4738,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4715,6 +4754,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4726,6 +4766,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4739,7 +4780,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4780,6 +4825,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11542,6 +11590,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19781,6 +19832,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7417eab6aef..096f29346d8 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 0b0977788f1..56d6740b9ea 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c59..152fd7ff086 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3273,6 +3273,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 24e0100c9f0..2f61be9c17e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3038,17 +3038,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6692,8 +6711,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6711,6 +6735,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index d59ed5f3fd0..bf3b0eb31c1 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2245,11 +2245,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3536,7 +3541,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..33b771990bd 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -163,7 +164,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +175,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7280e9836cf..61a0b2ccf38 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4235,6 +4235,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4243,6 +4244,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index b2ffe0a8c20..5d025328704 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -209,13 +209,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -234,8 +258,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1963,17 +2027,20 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
-ERROR:  syntax error at or near "ALL"
-LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
-                                            ^
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
-(1 row)
+Tables from schemas:
+    "public"
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1984,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2001,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2039,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 15b2b1cfd28..af31a2214ca 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,23 +1238,39 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#79Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Shlok Kyal (#78)
3 attachment(s)
Re: Skipping schema changes in publication

On Wed, 11 Jun 2025 at 19:37, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 17 Apr 2025 at 09:12, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Apr 16, 2025 at 8:22 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:

On Thu, Apr 10, 2025 at 7:25 PM Amit Kapila wrote:

On Tue, Jan 9, 2024 at 12:02 PM vignesh C <vignesh21@gmail.com> wrote:

As I did not see much interest from others, I'm withdrawing this patch
for now. But if there is any interest others in future, I would be
more than happy to work on this feature.

Just FYI, I noticed a use case for this patch in email [1]. Users would like to
replicate all except a few columns having sensitive information. The challenge
with current column list features is that adding new tables to columns would
lead users to change the respective publications as well.

[1] -
/messages/by-id/tencent_DCDF626FCD4A556C51BE
270FDC3047540208%40qq.com

BTW, I noticed that debezium, an open source distributed platform for change
data capture that replies on logical decoding, also support specifying the
column exclusion list[1]. So, this indicates that there could be some use cases
for this feature.

Thanks for sharing the link. I see that they support both the include
and exclude lists for columns and tables.

Hi Hackers,

I see there is some interest in the functionality added by this patch.
I have rebased the patches in [1]. I saw a new column 'pubgencols' was
added in pg_publication in PG 18. So, I have modified v11-0001 to
RESET this as well.
I am also working on creating a patch to exclude columns in
publication as per suggestion in [2].

[1]: /messages/by-id/CALDaNm3dWZCYDih55qTNAYsjCvYXMFv=46UsDWmfCnXMt3kPCg@mail.gmail.com
[2]: /messages/by-id/CAA4eK1KRdAPC=5=7tQ1GW0cRwD=zaDMi+T4u_k4GxPhPY6e8BQ@mail.gmail.com

I have attached a patch support excluding columns for publication.

I have added a syntax: "FOR TABLE table_name EXCEPT (c1, c2, ..)"
It can be used with CREATE or ALTER PUBLICATION.

v12-0003 patch contains the changes for the same.

Thanks and Regards,
Shlok Kyal

Attachments:

v12-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v12-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 9be85f8ffa1440a5d5c39fcd3df2672ca8e6d3dc Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v12 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 +++++--
 src/backend/commands/publicationcmds.c    | 111 ++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 120 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 ++++++++++
 7 files changed, 321 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..06452af9214 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +245,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..159dc3781d0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,94 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1598,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0b5652071d1..952e8e103cf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10762,6 +10762,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10808,6 +10810,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 620830feb9d..d59ed5f3fd0 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2242,7 +2242,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index dd00ab420b8..7280e9836cf 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4272,6 +4272,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..b2ffe0a8c20 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,126 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
+                                            ^
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..15b2b1cfd28 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v12-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v12-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From a51aee359bde6c2f8f83bee3f42c40a61984f76d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 19:08:35 +0530
Subject: [PATCH v12 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  16 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  97 ++++++++-
 src/test/regress/sql/publication.sql          |  47 ++++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++++
 25 files changed, 689 insertions(+), 134 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fa86c569dc4..4e37c928b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 686dd441d02..022c4fd4e2c 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2251,10 +2251,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 06452af9214..37e2c84bc10 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -89,8 +95,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -238,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..7fd8872db5f 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +458,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 8f7d8758ca0..3cce762fa97 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2094,8 +2094,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..ec580e3b050 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -861,13 +877,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +903,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +925,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1181,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 159dc3781d0..5194b2fb6e2 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1246,6 +1251,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1284,6 +1310,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1596,6 +1695,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1808,6 +1921,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1880,6 +1994,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1955,8 +2070,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d813..8a8268a05d2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8624,7 +8624,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
@@ -18794,7 +18794,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 952e8e103cf..89ac0495ce8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -585,6 +585,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10614,7 +10615,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10634,12 +10635,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10677,6 +10679,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10752,6 +10755,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10764,6 +10786,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10790,6 +10814,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 693a766e6d7..5512b4cba7f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2063,7 +2063,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2174,22 +2175,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2208,7 +2193,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2223,6 +2209,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2301,6 +2289,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..bffdab2ab63 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5845,14 +5847,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5890,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5908,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 37432e66efd..92db5ca8d97 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -183,6 +183,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4510,8 +4512,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4677,6 +4705,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4688,8 +4717,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4700,6 +4738,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4715,6 +4754,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4726,6 +4766,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4739,7 +4780,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4780,6 +4825,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11542,6 +11590,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19781,6 +19832,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7417eab6aef..096f29346d8 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 0b0977788f1..56d6740b9ea 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c59..152fd7ff086 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3273,6 +3273,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 24e0100c9f0..2f61be9c17e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3038,17 +3038,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6692,8 +6711,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6711,6 +6735,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index d59ed5f3fd0..bf3b0eb31c1 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2245,11 +2245,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3536,7 +3541,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..33b771990bd 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -163,7 +164,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +175,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7280e9836cf..61a0b2ccf38 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4235,6 +4235,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4243,6 +4244,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index b2ffe0a8c20..5d025328704 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -209,13 +209,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -234,8 +258,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1963,17 +2027,20 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
-ERROR:  syntax error at or near "ALL"
-LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
-                                            ^
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
-(1 row)
+Tables from schemas:
+    "public"
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1984,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2001,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2039,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 15b2b1cfd28..af31a2214ca 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,23 +1238,39 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v12-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v12-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 68403578523c96943c3616dbbdb2a82ad8863244 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 17 Jun 2025 12:12:24 +0530
Subject: [PATCH v12 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

This patch introduces syntax to exclude columns of tables from
publication. Syntax: FOR TABLE tabname EXCEPT (column_list)
It can be used with CREATE/ ALTER PUBLICATION. Eg:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (exclude_column_list)
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (exclude_column_list)
---
 doc/src/sgml/catalogs.sgml                    |  14 ++
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  18 ++-
 src/backend/catalog/pg_publication.c          | 135 +++++++++++++++++-
 src/backend/commands/publicationcmds.c        |  73 +++++++++-
 src/backend/parser/gram.y                     |  60 ++++++++
 src/backend/replication/pgoutput/pgoutput.c   |  47 +++++-
 src/bin/pg_dump/pg_dump.c                     |  39 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/include/catalog/pg_publication.h          |   9 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/test/regress/expected/publication.out     |  65 +++++++++
 src/test/regress/sql/publication.sql          |  45 ++++++
 .../t/036_rep_changes_except_table.pl         |  60 +++++++-
 15 files changed, 560 insertions(+), 18 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4e37c928b44..544998a1725 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6589,6 +6589,20 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        A null value indicates that all columns are published.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>prexcludeattrs</structfield> <type>int2vector</type>
+       (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+      </para>
+      <para>
+       This is an array of values that indicates which table columns are
+       excluded from the publication.  For example, a value of
+       <literal>1 3</literal> would mean that the columns except the first and
+       the third columns are published.
+       A null value indicates that no columns are excluded from being published.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 37e2c84bc10..70b04fc7320 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] { [ [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | [ EXCEPT ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] ] } [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 7fd8872db5f..bb44a20b28d 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] { [ [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | [ EXCEPT ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] ] } [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -103,6 +103,14 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
       lists.
      </para>
 
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. The excluded column list cannot contain generated columns. The
+      column list and excluded column list cannot be specified together.
+      Specifying a column list has no effect on <literal>TRUNCATE</literal>
+      commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -474,6 +482,14 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table <structname>users</structname>
+   except changes for columns <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ec580e3b050..8fd9ac84451 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -302,6 +302,53 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 	return found;
 }
 
+/*
+ * Returns true if the relation has exluded column list associated with the
+ * publication, false otherwise.
+ *
+ * If a exclude column list is found, the corresponding bitmap is returned
+ * through the cols parameter, if provided. The bitmap is constructed within the
+ * given memory context (mcxt).
+ */
+
+bool
+check_and_fetch_exclude_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
+									Bitmapset **cols)
+{
+	HeapTuple	cftuple;
+	bool		found = false;
+
+	if (pub->alltables)
+		return false;
+
+	cftuple = SearchSysCache2(PUBLICATIONRELMAP,
+							  ObjectIdGetDatum(relid),
+							  ObjectIdGetDatum(pub->oid));
+	if (HeapTupleIsValid(cftuple))
+	{
+		Datum		cfdatum;
+		bool		isnull;
+
+		/* Lookup the column list attribute. */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcludeattrs, &isnull);
+
+		/* Was a column list found? */
+		if (!isnull)
+		{
+			/* Build the column list bitmap in the given memory context. */
+			if (cols)
+				*cols = pub_collist_to_bitmapset(*cols, cfdatum, mcxt);
+
+			found = true;
+		}
+
+		ReleaseSysCache(cftuple);
+	}
+
+	return found;
+}
+
 /*
  * Gets the relations based on the publication partition option for a specified
  * relation.
@@ -449,6 +496,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	Oid			relid = RelationGetRelid(targetrel);
 	Oid			pubreloid;
 	Bitmapset  *attnums;
+	Bitmapset  *excludeattnums;
 	Publication *pub = GetPublication(pubid);
 	ObjectAddress myself,
 				referenced;
@@ -481,6 +529,13 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	/* Validate and translate column names into a Bitmapset of attnums. */
 	attnums = pub_collist_validate(pri->relation, pri->columns);
 
+	/*
+	 * Validate and translate excluded column names into a Bitmapset of
+	 * attnums.
+	 */
+	excludeattnums = pub_exclude_collist_validate(pri->relation,
+												  pri->exclude_columns);
+
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
@@ -507,6 +562,11 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	else
 		nulls[Anum_pg_publication_rel_prattrs - 1] = true;
 
+	if (pri->exclude_columns)
+		values[Anum_pg_publication_rel_prexcludeattrs - 1] = PointerGetDatum(attnumstoint2vector(excludeattnums));
+	else
+		nulls[Anum_pg_publication_rel_prexcludeattrs - 1] = true;
+
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
 	/* Insert tuple into catalog. */
@@ -609,6 +669,58 @@ pub_collist_validate(Relation targetrel, List *columns)
 	return set;
 }
 
+/*
+ * pub_exclude_collist_validate
+ *		Process and validate the 'excluded columns' list and ensure the columns
+ *		are all valid to exclude from publication.  Checks for and raises an
+ * 		ERROR for any unknown columns, system columns, duplicate columns, or
+ *		generated columns.
+ *
+ * Looks up each column's attnum and returns a 0-based Bitmapset of the
+ * corresponding attnums.
+ */
+Bitmapset *
+pub_exclude_collist_validate(Relation targetrel, List *exclude_columns)
+{
+	Bitmapset  *set = NULL;
+	ListCell   *lc;
+	TupleDesc	tupdesc = RelationGetDescr(targetrel);
+
+	foreach(lc, exclude_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 use system column \"%s\" in publication except column list",
+						   colname));
+
+		if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot use generated column \"%s\" in publication except column list",
+						   colname));
+
+		if (bms_is_member(attnum, set))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("duplicate column \"%s\" in publication except column list",
+						   colname));
+
+		set = bms_add_member(set, attnum);
+	}
+
+	return set;
+}
+
 /*
  * Transform a column list (represented by an array Datum) to a bitmapset.
  *
@@ -646,10 +758,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the excludecols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *excludecols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -672,6 +786,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (excludecols && bms_is_member(att->attnum, excludecols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -1263,6 +1380,9 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Datum		excludeattnums_datum;
+		Bitmapset  *excludeattnums = NULL;
+		bool		isnull;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1296,6 +1416,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
 										&(nulls[3]));
+
+			/* get the excluded column list */
+			excludeattnums_datum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+												   Anum_pg_publication_rel_prexcludeattrs,
+												   &isnull);
+			if (!isnull)
+				excludeattnums = pub_collist_to_bitmapset(NULL, excludeattnums_datum, NULL);
 		}
 		else
 		{
@@ -1335,6 +1462,10 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/* Skip columns that are part of excluded column list */
+				if (excludeattnums && bms_is_member(att->attnum, excludeattnums))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 5194b2fb6e2..e850c2345ea 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -358,7 +358,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and not part of excluded column list. If any column is
+ * 	  missing or is part of exclude column list, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -378,6 +379,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	Oid			publish_as_relid = RelationGetRelid(relation);
 	Bitmapset  *idattrs;
 	Bitmapset  *columns = NULL;
+	Bitmapset  *exclude_columns = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
@@ -405,11 +407,15 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
 	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_exclude_column_list(pub, publish_as_relid, NULL, &exclude_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
-		/* With REPLICA IDENTITY FULL, no column list is allowed. */
-		*invalid_column_list = (columns != NULL);
+		/*
+		 * With REPLICA IDENTITY FULL, no column list and no excluded column
+		 * list is allowed.
+		 */
+		*invalid_column_list = (columns != NULL || exclude_columns != NULL);
 
 		/*
 		 * As we don't allow a column list with REPLICA IDENTITY FULL, the
@@ -471,6 +477,16 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 				break;
 			}
 
+			/*
+			 * If REPLICA IDENTITY should not contain columns which are
+			 * excluded from the publication.
+			 */
+			if (exclude_columns && bms_is_member(att->attnum, exclude_columns))
+			{
+				*invalid_column_list = true;
+				break;
+			}
+
 			/* Skip validating the column list since it is not defined */
 			continue;
 		}
@@ -798,7 +814,7 @@ CheckPubRelationColumnList(char *pubname, List *tables,
 	{
 		PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc);
 
-		if (pri->columns == NIL)
+		if (pri->columns == NIL && pri->exclude_columns == NIL)
 			continue;
 
 		/*
@@ -1043,6 +1059,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 			char	   *relname;
 			bool		has_rowfilter;
 			bool		has_collist;
+			bool		has_exclude_collist;
 
 			/*
 			 * Beware: we don't have lock on the relations, so cope silently
@@ -1056,7 +1073,9 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 				continue;
 			has_rowfilter = !heap_attisnull(rftuple, Anum_pg_publication_rel_prqual, NULL);
 			has_collist = !heap_attisnull(rftuple, Anum_pg_publication_rel_prattrs, NULL);
-			if (!has_rowfilter && !has_collist)
+			has_exclude_collist = !heap_attisnull(rftuple, Anum_pg_publication_rel_prexcludeattrs, NULL);
+
+			if (!has_rowfilter && !has_collist && !has_exclude_collist)
 			{
 				ReleaseSysCache(rftuple);
 				continue;
@@ -1083,6 +1102,14 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 								stmt->pubname),
 						 errdetail("The publication contains a WHERE clause for partitioned table \"%s\", which is not allowed when \"%s\" is false.",
 								   relname, "publish_via_partition_root")));
+			if (has_exclude_collist)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("cannot set parameter \"%s\" to false for publication \"%s\"",
+								"publish_via_partition_root",
+								stmt->pubname),
+						 errdetail("The publication contains a except column list for partitioned table \"%s\", which is not allowed when \"%s\" is false.",
+								   relname, "publish_via_partition_root")));
 			Assert(has_collist);
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1443,6 +1470,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			Bitmapset  *oldexcludecolumns = NULL;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1458,6 +1486,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		excludeColumnListDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1474,6 +1503,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				/* Transform the int2vector exclude column list to a bitmap. */
+				excludeColumnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+														 Anum_pg_publication_rel_prexcludeattrs,
+														 &isnull);
+
+				if (!isnull)
+					oldexcludecolumns = pub_collist_to_bitmapset(NULL, excludeColumnListDatum, NULL);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1482,6 +1519,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				PublicationRelInfo *newpubrel;
 				Oid			newrelid;
 				Bitmapset  *newcolumns = NULL;
+				Bitmapset  *newexcludecolumns = NULL;
 
 				newpubrel = (PublicationRelInfo *) lfirst(newlc);
 				newrelid = RelationGetRelid(newpubrel->relation);
@@ -1495,6 +1533,9 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				newcolumns = pub_collist_validate(newpubrel->relation,
 												  newpubrel->columns);
 
+				newexcludecolumns = pub_collist_validate(newpubrel->relation,
+														 newpubrel->exclude_columns);
+
 				/*
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
@@ -1505,7 +1546,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						bms_equal(oldexcludecolumns, newexcludecolumns))
 					{
 						found = true;
 						break;
@@ -1522,6 +1564,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->exclude_columns = NIL;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1596,6 +1639,17 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 							   stmt->pubname),
 						errdetail("Schemas cannot be added if any tables that specify a column list are already part of the publication."));
 
+			/*
+			 * Disallow adding schema if exclude column list is already part
+			 * of the publication. See CheckPubRelationColumnList.
+			 */
+			if (!heap_attisnull(coltuple, Anum_pg_publication_rel_prexcludeattrs, NULL))
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("cannot add schema to publication \"%s\"",
+							   stmt->pubname),
+						errdetail("Schemas cannot be added if any tables that specify an except column list are already part of the publication."));
+
 			ReleaseSysCache(coltuple);
 		}
 
@@ -1922,6 +1976,7 @@ OpenTableList(List *tables)
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
 		pub_rel->except = t->except;
+		pub_rel->exclude_columns = t->exclude_columns;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1995,6 +2050,7 @@ OpenTableList(List *tables)
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
 				pub_rel->except = t->except;
+				pub_rel->exclude_columns = t->exclude_columns;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -2114,6 +2170,11 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 					errcode(ERRCODE_SYNTAX_ERROR),
 					errmsg("column list must not be specified in ALTER PUBLICATION ... DROP"));
 
+		if (pubrel->exclude_columns)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("except column list must not be specified in ALTER PUBLICATION ... DROP"));
+
 		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
 							   ObjectIdGetDatum(relid),
 							   ObjectIdGetDatum(pubid));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 89ac0495ce8..1be4298bce7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -447,6 +447,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list except_pub_obj_list
+				opt_exclude_column_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -4415,6 +4416,10 @@ opt_column_list:
 			| /*EMPTY*/								{ $$ = NIL; }
 		;
 
+opt_exclude_column_list:
+			'(' columnList ')'						{ $$ = $2; }
+		;
+
 columnList:
 			columnElem								{ $$ = list_make1($1); }
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
@@ -10681,6 +10686,15 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| TABLE relation_expr EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $2;
+					$$->pubtable->exclude_columns = $4;
+					$$->pubtable->whereClause = $5;
+				}
 			| TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10721,6 +10735,33 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
+			| ColId EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					/*
+					 * If either a row filter or exclude column list is
+					 * specified, create a PublicationTable object.
+					 */
+					if ($3 || $4)
+					{
+						/*
+						 * The OptWhereClause must be stored here but it is
+						 * valid only for tables. For non-table objects, an
+						 * error will be thrown later via
+						 * preprocess_pubobj_list().
+						 */
+						$$->pubtable = makeNode(PublicationTable);
+						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
+						$$->pubtable->exclude_columns = $3;
+						$$->pubtable->whereClause = $4;
+					}
+					else
+					{
+						$$->name = $1;
+					}
+					$$->location = @1;
+				}
 			| ColId indirection opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10731,6 +10772,16 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| ColId indirection EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
+					$$->pubtable->exclude_columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->location = @1;
+				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
 			| extended_relation_expr opt_column_list OptWhereClause
 				{
@@ -10741,6 +10792,15 @@ PublicationObjSpec:
 					$$->pubtable->columns = $2;
 					$$->pubtable->whereClause = $3;
 				}
+			| extended_relation_expr EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $1;
+					$$->pubtable->exclude_columns = $3;
+					$$->pubtable->whereClause = $4;
+				}
 			| CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 5512b4cba7f..f36c361abd5 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,9 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/* Indicate if no column is included in the publication */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1099,6 +1102,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	bool		first = true;
 	Relation	relation = RelationIdGetRelation(entry->publish_as_relid);
 	bool		found_pub_collist = false;
+	bool		found_pub_exclude_collist = false;
 	Bitmapset  *relcols = NULL;
 
 	pgoutput_ensure_entry_cxt(data, entry);
@@ -1120,12 +1124,32 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		Bitmapset  *excludecols = NULL;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
 														 entry->entry_cxt, &cols);
 
+		/* Retrieve the bitmap of exclude columns for the publication. */
+		found_pub_exclude_collist |= check_and_fetch_exclude_column_list(pub,
+																		 entry->publish_as_relid,
+																		 entry->entry_cxt, &excludecols);
+
+		/*
+		 * cols and exclude cols can't appear together. Syntax for it is not
+		 * supported. If column list is not present check for excluded column
+		 * list and construct a corresponding column list.
+		 */
+		if (!cols && found_pub_exclude_collist)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, excludecols);
+			MemoryContextSwitchTo(oldcxt);
+		}
+
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
 		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
@@ -1144,7 +1168,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1155,8 +1179,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		{
 			entry->columns = cols;
 			first = false;
+
+			if (excludecols && !cols)
+				entry->no_cols_published = true;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published && cols) || !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1165,10 +1192,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	}							/* loop all subscribed publications */
 
 	/*
-	 * If no column list publications exist, columns to be published will be
-	 * computed later according to the 'publish_generated_columns' parameter.
+	 * If no column list or excluded column list publications exist, columns
+	 * to be published will be computed later according to the
+	 * 'publish_generated_columns' parameter.
 	 */
-	if (!found_pub_collist)
+	if (!found_pub_collist && !found_pub_exclude_collist)
 		entry->columns = NULL;
 
 	RelationClose(relation);
@@ -1480,6 +1508,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table is present in the exclude column list. Skip
+	 * publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2057,6 +2092,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2106,6 +2142,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 92db5ca8d97..29364603130 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4706,6 +4706,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelqual;
 	int			i_prattrs;
 	int			i_prexcept;
+	int			i_prexcludeattrs;
 	int			i,
 				j,
 				ntups;
@@ -4723,7 +4724,15 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 		/* FIXME: 180000 should be changed to 190000 later for PG19. */
 		if (fout->remoteVersion >= 180000)
-			appendPQExpBufferStr(query, " prexcept,\n");
+			appendPQExpBufferStr(query, " prexcept, "
+								 "(CASE\n"
+								 "  WHEN pr.prexcludeattrs IS NOT NULL THEN\n"
+								 "    (SELECT array_agg(attname)\n"
+								 "       FROM\n"
+								 "         pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prexcludeattrs::pg_catalog.int2[], 1)) s,\n"
+								 "         pg_catalog.pg_attribute\n"
+								 "      WHERE attrelid = pr.prrelid AND attnum = prexcludeattrs[s])\n"
+								 "  ELSE NULL END) prexcludeattrs, \n");
 		else
 			appendPQExpBufferStr(query, " false AS prexcept,\n");
 
@@ -4755,6 +4764,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
 	i_prexcept = PQfnumber(res, "prexcept");
+	i_prexcludeattrs = PQfnumber(res, "prexcludeattrs");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4822,6 +4832,30 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		if (!PQgetisnull(res, i, i_prexcludeattrs))
+		{
+			char	  **attnames;
+			int			nattnames;
+			PQExpBuffer excludeattribs;
+
+			if (!parsePGArray(PQgetvalue(res, i, i_prexcludeattrs),
+							  &attnames, &nattnames))
+				pg_fatal("could not parse %s array", "prattrs");
+			excludeattribs = createPQExpBuffer();
+			for (int k = 0; k < nattnames; k++)
+			{
+				if (k > 0)
+					appendPQExpBufferStr(excludeattribs, ", ");
+
+				appendPQExpBufferStr(excludeattribs, fmtId(attnames[k]));
+			}
+			pubrinfo[j].pubrexcludeattrs = excludeattribs->data;
+			free(excludeattribs);	/* but not excludeattribs->data */
+			free(attnames);
+		}
+		else
+			pubrinfo[j].pubrexcludeattrs = NULL;
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
@@ -4907,6 +4941,9 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	if (pubrinfo->pubrattrs)
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
 
+	if (pubrinfo->pubrexcludeattrs)
+		appendPQExpBuffer(query, " EXCEPT (%s)", pubrinfo->pubrexcludeattrs);
+
 	if (pubrinfo->pubrelqual)
 	{
 		/*
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 096f29346d8..e01c2d1afbd 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -681,6 +681,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	char	   *pubrexcludeattrs;
 } PublicationRelInfo;
 
 /*
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 33b771990bd..5344559c88e 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -140,6 +140,7 @@ typedef struct PublicationRelInfo
 	Node	   *whereClause;
 	List	   *columns;
 	bool		except;
+	List	   *exclude_columns;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
@@ -181,15 +182,21 @@ extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
 										MemoryContext mcxt, Bitmapset **cols);
+extern bool check_and_fetch_exclude_column_list(Publication *pub, Oid relid,
+												MemoryContext mcxt,
+												Bitmapset **cols);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
+extern Bitmapset *pub_exclude_collist_validate(Relation targetrel,
+											   List *exclude_columns);
 extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 											bool if_not_exists);
 
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *excludecols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..4c1b4ddbddc 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -36,6 +36,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
 	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prexcludeattrs; /* columns to exclude */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 61a0b2ccf38..f148c8e2323 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4236,6 +4236,7 @@ typedef struct PublicationTable
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
 	bool		except;			/* exclude the relation */
+	List	   *exclude_columns;	/* List of columns to be excluded */
 } PublicationTable;
 
 /*
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5d025328704..a274b3cff31 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,71 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+CREATE TABLE pub_test_except3 (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  syntax error at or near ";"
+LINE 1: ...BLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+                                                                      ^
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen1);
+ERROR:  cannot use generated column "gen1" in publication except column list
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {c,d}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  except column list must not be specified in ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
+DROP TABLE pub_test_except3;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index af31a2214ca..6b23f215739 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,51 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+CREATE TABLE pub_test_except3 (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen1);
+
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
+DROP TABLE pub_test_except3;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
index 1d115283809..ec77f2e8d04 100644
--- a/src/test/subscription/t/036_rep_changes_except_table.pl
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -1,7 +1,7 @@
 
 # Copyright (c) 2021-2022, PostgreSQL Global Development Group
 
-# Logical replication tests for except table publications
+# Logical replication tests for except table and except column publications
 use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
@@ -77,6 +77,64 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*), min(a), max(a) FROM public.tab1");
 is($result, qq(0||), 'check rows on subscriber catchup');
 
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2 EXCEPT (b, c)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+	'check that initial sync for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is($result, qq(1||), 'check that initial sync for except column publication');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+# Test incremental changes
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is( $result, qq(1||
+4||), 'check incremental insert for except column publication');
+
+# Test for update
+$node_publisher->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_subscriber->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET a = 3, b = 4, c = 5 WHERE a = 1");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|5|6
+|4|5),
+	'check update for except column publication');
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
-- 
2.34.1

#80Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#79)
Re: Skipping schema changes in publication

On Tue, Jun 17, 2025 at 5:41 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
...

I have attached a patch support excluding columns for publication.

I have added a syntax: "FOR TABLE table_name EXCEPT (c1, c2, ..)"
It can be used with CREATE or ALTER PUBLICATION.

v12-0003 patch contains the changes for the same.

Hi Shlok,

I was interested in your new EXCEPT (col-list) so I had a quick look
at your patch v12-0003 (only looked at the documentation).

Below are some comments:

======

1. Chapter 29.5 "Column Lists".

I think new EXCEPT syntax needs a mention here as well.

======

doc/src/sgml/catalogs.sgml

2.
+      <para>
+       This is an array of values that indicates which table columns are
+       excluded from the publication.  For example, a value of
+       <literal>1 3</literal> would mean that the columns except the first and
+       the third columns are published.
+       A null value indicates that no columns are excluded from being
published.
+      </para></entry>

The sentence "A null value indicates that no columns are excluded from
being published" seems kind of confusing, because if the user has a
"normal" column-list although nothing was being *explicitly* excluded
(using EXCEPT), any columns not named are *implicitly* excluded from
being published.

~

3.
TBH, I was wondering why a new catalog attribute was necessary...

Can't you simply re-use the existing attribute "prattrs" attribute.
e.g. let's just define negative means exclude.

e.g. a value of 1 3 means only the 1st and 3rd columns are published
e.g. a value of -1 -3 means all columns except 1st and 3rd columns are published
e.g. a value of null mean all columns are published

(mixes of negative and positive will not be possible)

======

doc/src/sgml/ref/alter_publication.sgml

4. ALTER PUBLICATION syntax

The syntax is currently written as:
TABLE [ ONLY ] table_name [ * ] { [ [ ( column_name [, ... ] ) ] | [
EXCEPT ( column_name [, ... ] ) ] ] } [ WHERE ( expression ) ] [, ...
]

Can't this be more simply written as:
TABLE [ ONLY ] table_name [ * ] [ [ EXCEPT ] ( column_name [, ... ] )
] [ WHERE ( expression ) ] [, ... ]

~~~

5.
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);

Those tags don't seem correct. e.g. "users" and "security_pin" are not
<structname> (???).

Perhaps, every other example here is wrong too and you just copied
them? Anyway, something here looks wrong to me.

======
doc/src/sgml/ref/create_publication.sgml

6. CREATE PUBLICATION syntax

The syntax is currently written as:
TABLE [ ONLY ] table_name [ * ] { [ [ ( column_name [, ... ] ) ] | [
EXCEPT ( column_name [, ... ] ) ] ] } [ WHERE ( expression ) ] [, ...
]

Can't this be more simply written as:
TABLE [ ONLY ] table_name [ * ] [ [ EXCEPT ] ( column_name [, ... ] )
] [ WHERE ( expression ) ] [, ... ]

~~~

7.
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. The excluded column list cannot contain generated
columns. The
+      column list and excluded column list cannot be specified together.
+      Specifying a column list has no effect on <literal>TRUNCATE</literal>
+      commands.
+     </para>

IMO you don't need to say "The column list and excluded column list
cannot be specified together." because AFAIK the syntax makes that
impossible to do anyhow.

~~~

8.
+  <para>
+   Create a publication that publishes all changes for table
<structname>users</structname>
+   except changes for columns <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>

8a.
Same review comment as previously -- Those tags don't seem correct.
e.g. "users" and "security_pin" are not <structname> (???).
Again, are all the other existing tags also wrong? Maybe a new thread
needed to address these?

~

8b.
Plural? /except changes for columns/except changes for column/

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

#81Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#80)
3 attachment(s)
Re: Skipping schema changes in publication

On Wed, 18 Jun 2025 at 06:34, Peter Smith <smithpb2250@gmail.com> wrote:

On Tue, Jun 17, 2025 at 5:41 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
...

I have attached a patch support excluding columns for publication.

I have added a syntax: "FOR TABLE table_name EXCEPT (c1, c2, ..)"
It can be used with CREATE or ALTER PUBLICATION.

v12-0003 patch contains the changes for the same.

Hi Shlok,

I was interested in your new EXCEPT (col-list) so I had a quick look
at your patch v12-0003 (only looked at the documentation).

Below are some comments:

======

1. Chapter 29.5 "Column Lists".

I think new EXCEPT syntax needs a mention here as well.

Added

======

doc/src/sgml/catalogs.sgml

2.
+      <para>
+       This is an array of values that indicates which table columns are
+       excluded from the publication.  For example, a value of
+       <literal>1 3</literal> would mean that the columns except the first and
+       the third columns are published.
+       A null value indicates that no columns are excluded from being
published.
+      </para></entry>

The sentence "A null value indicates that no columns are excluded from
being published" seems kind of confusing, because if the user has a
"normal" column-list although nothing was being *explicitly* excluded
(using EXCEPT), any columns not named are *implicitly* excluded from
being published.

I have removed this line.

~

3.
TBH, I was wondering why a new catalog attribute was necessary...

Can't you simply re-use the existing attribute "prattrs" attribute.
e.g. let's just define negative means exclude.

e.g. a value of 1 3 means only the 1st and 3rd columns are published
e.g. a value of -1 -3 means all columns except 1st and 3rd columns are published
e.g. a value of null mean all columns are published

(mixes of negative and positive will not be possible)

Currently I have added a new attribute 'prexcludeattrs' in
pg_publication_rel table. I used this approach because it will be
easier for user to get the exclude column list, in code no extra
processing is required to get the exclude column list.

For an approach to use negative numbers for exclude columns. I see an
advantage that we do not need to introduce a new column for
pg_publication_rel. But in code, each time we want to get a column
list or exclude column list we need an extra processing of 'prattrs'
columns. Also I don't see any existing catalog table using a negative
attribute for column list.

Based on above observations, I feel that the current is better.

Please correct me if I missed an advantage for the approach you suggested.

======

doc/src/sgml/ref/alter_publication.sgml

4. ALTER PUBLICATION syntax

The syntax is currently written as:
TABLE [ ONLY ] table_name [ * ] { [ [ ( column_name [, ... ] ) ] | [
EXCEPT ( column_name [, ... ] ) ] ] } [ WHERE ( expression ) ] [, ...
]

Can't this be more simply written as:
TABLE [ ONLY ] table_name [ * ] [ [ EXCEPT ] ( column_name [, ... ] )
] [ WHERE ( expression ) ] [, ... ]

~~~

Fixed

5.
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);

Those tags don't seem correct. e.g. "users" and "security_pin" are not
<structname> (???).

Perhaps, every other example here is wrong too and you just copied
them? Anyway, something here looks wrong to me.

I saw different documents and usage of tags seems not well defined.
For example for table we are using tags in document
create_publication.sgml, update.sgml <structname> is used, in document
table.sgml, advanced.sgml <classname> is used, and in
logical-replication.sgml <literal> is used. Similarly for column
names <structname>, <structfield> or <literal> are used in different
parts of the document.

I kept the changed tag to <structfield> for the column for this patch.
Do you have any suggestions?

======
doc/src/sgml/ref/create_publication.sgml

6. CREATE PUBLICATION syntax

The syntax is currently written as:
TABLE [ ONLY ] table_name [ * ] { [ [ ( column_name [, ... ] ) ] | [
EXCEPT ( column_name [, ... ] ) ] ] } [ WHERE ( expression ) ] [, ...
]

Can't this be more simply written as:
TABLE [ ONLY ] table_name [ * ] [ [ EXCEPT ] ( column_name [, ... ] )
] [ WHERE ( expression ) ] [, ... ]

~~~

Fixed

7.
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. The excluded column list cannot contain generated
columns. The
+      column list and excluded column list cannot be specified together.
+      Specifying a column list has no effect on <literal>TRUNCATE</literal>
+      commands.
+     </para>

IMO you don't need to say "The column list and excluded column list
cannot be specified together." because AFAIK the syntax makes that
impossible to do anyhow.

Removed this line

~~~

8.
+  <para>
+   Create a publication that publishes all changes for table
<structname>users</structname>
+   except changes for columns <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>

8a.
Same review comment as previously -- Those tags don't seem correct.
e.g. "users" and "security_pin" are not <structname> (???).
Again, are all the other existing tags also wrong? Maybe a new thread
needed to address these?

~

Same as point 5.
I also feel this should be addressed in a new thread.

8b.
Plural? /except changes for columns/except changes for column/

Fixed

Also in this patch I added displaying "EXCEPT (column_list)" for \dRp+
and \d table_name psql commands.

Thanks and Regards,
Shlok Kyal

Attachments:

v13-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v13-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 85ae9bd9a095a7d9d63044aebaa7057422c013f1 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v13 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 +++++--
 src/backend/commands/publicationcmds.c    | 111 ++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 120 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 ++++++++++
 7 files changed, 321 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..06452af9214 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +245,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..159dc3781d0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,94 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1598,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 50f53159d58..e16f4832963 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10760,6 +10760,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10806,6 +10808,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 2c0b4f28c14..23cb27b4b05 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2242,7 +2242,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ba12678d1cb..905b58e0279 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4272,6 +4272,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..b2ffe0a8c20 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,126 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
+                                            ^
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..15b2b1cfd28 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v13-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v13-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 6b7c23e4194958c581d4476090442d44c37d702f Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 17 Jun 2025 12:12:24 +0530
Subject: [PATCH v13 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. THe publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (exclude_column_list)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (exclude_column_list)

A new column "prexcludeattrs" is added to table "pg_publication_rel", to
maintain the column list that user wants to exclude from the
publication.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of command can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
ans.
---
 doc/src/sgml/catalogs.sgml                    |  13 ++
 doc/src/sgml/logical-replication.sgml         | 145 ++++++++++++------
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  17 +-
 src/backend/catalog/pg_publication.c          | 135 +++++++++++++++-
 src/backend/commands/publicationcmds.c        |  73 ++++++++-
 src/backend/parser/gram.y                     |  60 ++++++++
 src/backend/replication/pgoutput/pgoutput.c   |  47 +++++-
 src/bin/pg_dump/pg_dump.c                     |  39 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 104 +++++++++----
 src/include/catalog/pg_publication.h          |   9 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/test/regress/expected/publication.out     |  65 ++++++++
 src/test/regress/sql/publication.sql          |  45 ++++++
 .../t/036_rep_changes_except_table.pl         |  60 +++++++-
 17 files changed, 735 insertions(+), 90 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4e37c928b44..b9e13b33064 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6589,6 +6589,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        A null value indicates that all columns are published.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>prexcludeattrs</structfield> <type>int2vector</type>
+       (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+      </para>
+      <para>
+       This is an array of values that indicates which table columns are
+       excluded from the publication.  For example, a value of
+       <literal>1 3</literal> would mean that the columns except the first and
+       the third columns are published.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 3d0d29cf8b1..318b4c43cfc 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1340,11 +1340,14 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
-   See <xref linkend="sql-createpublication"/> for details on the syntax.
+   Each publication can optionally either specify which columns of each table
+   are replicated to subscribers or specify which columns of each table are
+   excluded from replication to subscriber. The table on the subscriber side
+   must have at least all the columns that are published. If no column list and
+   no exclude column list are specified, then all columns on the publisher are
+   replicated. If a exclude column list is specified all the columns except the
+   specified columns are replicated. See <xref linkend="sql-createpublication"/>
+   for details on the syntax.
   </para>
 
   <para>
@@ -1359,56 +1362,65 @@ Publications:
    If no column list is specified, any columns added to the table later are
    automatically replicated. This means that having a column list which names
    all columns is not the same as having no column list at all.
+   If a exclude column list is specified, any columns added to the table later
+   are automatically replicated.
   </para>
 
   <para>
-   A column list can contain only simple column references.  The order
-   of columns in the list is not preserved.
+   A column list or exclude column list can contain only simple column
+   references.  The order of columns in the list is not preserved.
   </para>
 
   <para>
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
-   <literal>publish_generated_columns</literal></link>. See
+   <literal>publish_generated_columns</literal></link>. Generated columns cannot
+   be specified in a exclude column list. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
 
   <para>
-   Specifying a column list when the publication also publishes
-   <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
-   is not supported.
+   Specifying a column list or a exclude column list when the publication also
+   publishes <link linkend="sql-createpublication-params-for-tables-in-schema">
+   <literal>FOR TABLES IN SCHEMA</literal></link> is not supported.
   </para>
 
   <para>
    For partitioned tables, the publication parameter
-   <link linkend="sql-createpublication-params-with-publish-via-partition-root"><literal>publish_via_partition_root</literal></link>
-   determines which column list is used. If <literal>publish_via_partition_root</literal>
-   is <literal>true</literal>, the root partitioned table's column list is
-   used. Otherwise, if <literal>publish_via_partition_root</literal> is
-   <literal>false</literal> (the default), each partition's column list is used.
+   <link linkend="sql-createpublication-params-with-publish-via-partition-root">
+   <literal>publish_via_partition_root</literal></link> determines which column
+   list or exclude column list is used. If
+   <literal>publish_via_partition_root</literal> is <literal>true</literal>, the
+   root partitioned table's column list or exclude column list is used.
+   Otherwise, if <literal>publish_via_partition_root</literal> is
+   <literal>false</literal> (the default), each partition's column list or
+   exclude column list is used.
   </para>
 
   <para>
    If a publication publishes <command>UPDATE</command> or
    <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
-   If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   table's replica identity columns or any exclude column list must not include
+   the table's replica identity columns (see
+   <xref linkend="sql-altertable-replica-identity"/>). If a publication
+   publishes only <command>INSERT</command> operations, then the column list may
+   omit replica identity columns or exlude column list may contain replica
+   identity columns.
   </para>
 
   <para>
-   Column lists have no effect for the <literal>TRUNCATE</literal> command.
+   Column lists or exclude column lists have no effect for the
+   <literal>TRUNCATE</literal> command.
   </para>
 
   <para>
    During initial data synchronization, only the published columns are
    copied.  However, if the subscriber is from a release prior to 15, then
    all the columns in the table are copied during initial data synchronization,
-   ignoring any column lists. If the subscriber is from a release prior to 18,
-   then initial table synchronization won't copy generated columns even if they
-   are defined in the publisher.
+   ignoring any column lists or exclude column list. If the subscriber is from a
+   release prior to 18, then initial table synchronization won't copy generated
+   columns even if they are defined in the publisher.
   </para>
 
    <warning id="logical-replication-col-list-combining">
@@ -1416,21 +1428,23 @@ Publications:
     <para>
      There's currently no support for subscriptions comprising several
      publications where the same table has been published with different
-     column lists.  <xref linkend="sql-createsubscription"/> disallows
+     column lists or exclude column list.
+     <xref linkend="sql-createsubscription"/> disallows
      creating such subscriptions, but it is still possible to get into
-     that situation by adding or altering column lists on the publication
-     side after a subscription has been created.
+     that situation by adding or altering column lists or exclude column lists
+     on the publication side after a subscription has been created.
     </para>
     <para>
-     This means changing the column lists of tables on publications that are
-     already subscribed could lead to errors being thrown on the subscriber
-     side.
+     This means changing the column lists or exclude column list of tables on
+     publications that are already subscribed could lead to errors being thrown
+     on the subscriber side.
     </para>
     <para>
      If a subscription is affected by this problem, the only way to resume
-     replication is to adjust one of the column lists on the publication
-     side so that they all match; and then either recreate the subscription,
-     or use <link linkend="sql-altersubscription-params-setadddrop-publication">
+     replication is to adjust one of the column lists or exclude column lists on
+     the publication side so that they all match; and then either recreate the
+     subscription, or use
+     <link linkend="sql-altersubscription-params-setadddrop-publication">
      <literal>ALTER SUBSCRIPTION ... DROP PUBLICATION</literal></link> to
      remove one of the offending publications and add it again.
     </para>
@@ -1440,18 +1454,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal>, <literal>t2</literal> to be used in the
+    following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal> and a exclude column list is defined for table
+    <literal>t2</literal> to reduce the number of columns that will be
+    replicated. Notice that the order of column names in the column list or
+    exclude column list does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1459,12 +1476,13 @@ Publications:
      for each publication.
 <programlisting>
 /* pub # */ \dRp+
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+ Owner  | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
+--------+------------+---------+---------+---------+-----------+-------------------+----------
+ ubuntu | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1485,23 +1503,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1513,6 +1549,16 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
@@ -1526,6 +1572,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 37e2c84bc10..5700bf83100 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structfield>security_pin</structfield>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 7fd8872db5f..af46d6a7919 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -103,6 +103,13 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
       lists.
      </para>
 
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. The excluded column list cannot contain generated columns.
+      Specifying a column list has no effect on <literal>TRUNCATE</literal>
+      commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -474,6 +481,14 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table <structname>users</structname>
+   except changes for column <structfield>security_pin</structfield>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ec580e3b050..8fd9ac84451 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -302,6 +302,53 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 	return found;
 }
 
+/*
+ * Returns true if the relation has exluded column list associated with the
+ * publication, false otherwise.
+ *
+ * If a exclude column list is found, the corresponding bitmap is returned
+ * through the cols parameter, if provided. The bitmap is constructed within the
+ * given memory context (mcxt).
+ */
+
+bool
+check_and_fetch_exclude_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
+									Bitmapset **cols)
+{
+	HeapTuple	cftuple;
+	bool		found = false;
+
+	if (pub->alltables)
+		return false;
+
+	cftuple = SearchSysCache2(PUBLICATIONRELMAP,
+							  ObjectIdGetDatum(relid),
+							  ObjectIdGetDatum(pub->oid));
+	if (HeapTupleIsValid(cftuple))
+	{
+		Datum		cfdatum;
+		bool		isnull;
+
+		/* Lookup the column list attribute. */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcludeattrs, &isnull);
+
+		/* Was a column list found? */
+		if (!isnull)
+		{
+			/* Build the column list bitmap in the given memory context. */
+			if (cols)
+				*cols = pub_collist_to_bitmapset(*cols, cfdatum, mcxt);
+
+			found = true;
+		}
+
+		ReleaseSysCache(cftuple);
+	}
+
+	return found;
+}
+
 /*
  * Gets the relations based on the publication partition option for a specified
  * relation.
@@ -449,6 +496,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	Oid			relid = RelationGetRelid(targetrel);
 	Oid			pubreloid;
 	Bitmapset  *attnums;
+	Bitmapset  *excludeattnums;
 	Publication *pub = GetPublication(pubid);
 	ObjectAddress myself,
 				referenced;
@@ -481,6 +529,13 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	/* Validate and translate column names into a Bitmapset of attnums. */
 	attnums = pub_collist_validate(pri->relation, pri->columns);
 
+	/*
+	 * Validate and translate excluded column names into a Bitmapset of
+	 * attnums.
+	 */
+	excludeattnums = pub_exclude_collist_validate(pri->relation,
+												  pri->exclude_columns);
+
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
@@ -507,6 +562,11 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	else
 		nulls[Anum_pg_publication_rel_prattrs - 1] = true;
 
+	if (pri->exclude_columns)
+		values[Anum_pg_publication_rel_prexcludeattrs - 1] = PointerGetDatum(attnumstoint2vector(excludeattnums));
+	else
+		nulls[Anum_pg_publication_rel_prexcludeattrs - 1] = true;
+
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
 	/* Insert tuple into catalog. */
@@ -609,6 +669,58 @@ pub_collist_validate(Relation targetrel, List *columns)
 	return set;
 }
 
+/*
+ * pub_exclude_collist_validate
+ *		Process and validate the 'excluded columns' list and ensure the columns
+ *		are all valid to exclude from publication.  Checks for and raises an
+ * 		ERROR for any unknown columns, system columns, duplicate columns, or
+ *		generated columns.
+ *
+ * Looks up each column's attnum and returns a 0-based Bitmapset of the
+ * corresponding attnums.
+ */
+Bitmapset *
+pub_exclude_collist_validate(Relation targetrel, List *exclude_columns)
+{
+	Bitmapset  *set = NULL;
+	ListCell   *lc;
+	TupleDesc	tupdesc = RelationGetDescr(targetrel);
+
+	foreach(lc, exclude_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 use system column \"%s\" in publication except column list",
+						   colname));
+
+		if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot use generated column \"%s\" in publication except column list",
+						   colname));
+
+		if (bms_is_member(attnum, set))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("duplicate column \"%s\" in publication except column list",
+						   colname));
+
+		set = bms_add_member(set, attnum);
+	}
+
+	return set;
+}
+
 /*
  * Transform a column list (represented by an array Datum) to a bitmapset.
  *
@@ -646,10 +758,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the excludecols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *excludecols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -672,6 +786,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (excludecols && bms_is_member(att->attnum, excludecols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -1263,6 +1380,9 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Datum		excludeattnums_datum;
+		Bitmapset  *excludeattnums = NULL;
+		bool		isnull;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1296,6 +1416,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
 										&(nulls[3]));
+
+			/* get the excluded column list */
+			excludeattnums_datum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+												   Anum_pg_publication_rel_prexcludeattrs,
+												   &isnull);
+			if (!isnull)
+				excludeattnums = pub_collist_to_bitmapset(NULL, excludeattnums_datum, NULL);
 		}
 		else
 		{
@@ -1335,6 +1462,10 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/* Skip columns that are part of excluded column list */
+				if (excludeattnums && bms_is_member(att->attnum, excludeattnums))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 5194b2fb6e2..e850c2345ea 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -358,7 +358,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and not part of excluded column list. If any column is
+ * 	  missing or is part of exclude column list, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -378,6 +379,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	Oid			publish_as_relid = RelationGetRelid(relation);
 	Bitmapset  *idattrs;
 	Bitmapset  *columns = NULL;
+	Bitmapset  *exclude_columns = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
@@ -405,11 +407,15 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
 	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_exclude_column_list(pub, publish_as_relid, NULL, &exclude_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
-		/* With REPLICA IDENTITY FULL, no column list is allowed. */
-		*invalid_column_list = (columns != NULL);
+		/*
+		 * With REPLICA IDENTITY FULL, no column list and no excluded column
+		 * list is allowed.
+		 */
+		*invalid_column_list = (columns != NULL || exclude_columns != NULL);
 
 		/*
 		 * As we don't allow a column list with REPLICA IDENTITY FULL, the
@@ -471,6 +477,16 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 				break;
 			}
 
+			/*
+			 * If REPLICA IDENTITY should not contain columns which are
+			 * excluded from the publication.
+			 */
+			if (exclude_columns && bms_is_member(att->attnum, exclude_columns))
+			{
+				*invalid_column_list = true;
+				break;
+			}
+
 			/* Skip validating the column list since it is not defined */
 			continue;
 		}
@@ -798,7 +814,7 @@ CheckPubRelationColumnList(char *pubname, List *tables,
 	{
 		PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc);
 
-		if (pri->columns == NIL)
+		if (pri->columns == NIL && pri->exclude_columns == NIL)
 			continue;
 
 		/*
@@ -1043,6 +1059,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 			char	   *relname;
 			bool		has_rowfilter;
 			bool		has_collist;
+			bool		has_exclude_collist;
 
 			/*
 			 * Beware: we don't have lock on the relations, so cope silently
@@ -1056,7 +1073,9 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 				continue;
 			has_rowfilter = !heap_attisnull(rftuple, Anum_pg_publication_rel_prqual, NULL);
 			has_collist = !heap_attisnull(rftuple, Anum_pg_publication_rel_prattrs, NULL);
-			if (!has_rowfilter && !has_collist)
+			has_exclude_collist = !heap_attisnull(rftuple, Anum_pg_publication_rel_prexcludeattrs, NULL);
+
+			if (!has_rowfilter && !has_collist && !has_exclude_collist)
 			{
 				ReleaseSysCache(rftuple);
 				continue;
@@ -1083,6 +1102,14 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 								stmt->pubname),
 						 errdetail("The publication contains a WHERE clause for partitioned table \"%s\", which is not allowed when \"%s\" is false.",
 								   relname, "publish_via_partition_root")));
+			if (has_exclude_collist)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("cannot set parameter \"%s\" to false for publication \"%s\"",
+								"publish_via_partition_root",
+								stmt->pubname),
+						 errdetail("The publication contains a except column list for partitioned table \"%s\", which is not allowed when \"%s\" is false.",
+								   relname, "publish_via_partition_root")));
 			Assert(has_collist);
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1443,6 +1470,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			Bitmapset  *oldexcludecolumns = NULL;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1458,6 +1486,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		excludeColumnListDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1474,6 +1503,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				/* Transform the int2vector exclude column list to a bitmap. */
+				excludeColumnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+														 Anum_pg_publication_rel_prexcludeattrs,
+														 &isnull);
+
+				if (!isnull)
+					oldexcludecolumns = pub_collist_to_bitmapset(NULL, excludeColumnListDatum, NULL);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1482,6 +1519,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				PublicationRelInfo *newpubrel;
 				Oid			newrelid;
 				Bitmapset  *newcolumns = NULL;
+				Bitmapset  *newexcludecolumns = NULL;
 
 				newpubrel = (PublicationRelInfo *) lfirst(newlc);
 				newrelid = RelationGetRelid(newpubrel->relation);
@@ -1495,6 +1533,9 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				newcolumns = pub_collist_validate(newpubrel->relation,
 												  newpubrel->columns);
 
+				newexcludecolumns = pub_collist_validate(newpubrel->relation,
+														 newpubrel->exclude_columns);
+
 				/*
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
@@ -1505,7 +1546,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						bms_equal(oldexcludecolumns, newexcludecolumns))
 					{
 						found = true;
 						break;
@@ -1522,6 +1564,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->exclude_columns = NIL;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1596,6 +1639,17 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 							   stmt->pubname),
 						errdetail("Schemas cannot be added if any tables that specify a column list are already part of the publication."));
 
+			/*
+			 * Disallow adding schema if exclude column list is already part
+			 * of the publication. See CheckPubRelationColumnList.
+			 */
+			if (!heap_attisnull(coltuple, Anum_pg_publication_rel_prexcludeattrs, NULL))
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("cannot add schema to publication \"%s\"",
+							   stmt->pubname),
+						errdetail("Schemas cannot be added if any tables that specify an except column list are already part of the publication."));
+
 			ReleaseSysCache(coltuple);
 		}
 
@@ -1922,6 +1976,7 @@ OpenTableList(List *tables)
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
 		pub_rel->except = t->except;
+		pub_rel->exclude_columns = t->exclude_columns;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1995,6 +2050,7 @@ OpenTableList(List *tables)
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
 				pub_rel->except = t->except;
+				pub_rel->exclude_columns = t->exclude_columns;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -2114,6 +2170,11 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 					errcode(ERRCODE_SYNTAX_ERROR),
 					errmsg("column list must not be specified in ALTER PUBLICATION ... DROP"));
 
+		if (pubrel->exclude_columns)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("except column list must not be specified in ALTER PUBLICATION ... DROP"));
+
 		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
 							   ObjectIdGetDatum(relid),
 							   ObjectIdGetDatum(pubid));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7fe95a840f..63ee4bb7079 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,6 +446,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list except_pub_obj_list
+				opt_exclude_column_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -4413,6 +4414,10 @@ opt_column_list:
 			| /*EMPTY*/								{ $$ = NIL; }
 		;
 
+opt_exclude_column_list:
+			'(' columnList ')'						{ $$ = $2; }
+		;
+
 columnList:
 			columnElem								{ $$ = list_make1($1); }
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
@@ -10679,6 +10684,15 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| TABLE relation_expr EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $2;
+					$$->pubtable->exclude_columns = $4;
+					$$->pubtable->whereClause = $5;
+				}
 			| TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10719,6 +10733,33 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
+			| ColId EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					/*
+					 * If either a row filter or exclude column list is
+					 * specified, create a PublicationTable object.
+					 */
+					if ($3 || $4)
+					{
+						/*
+						 * The OptWhereClause must be stored here but it is
+						 * valid only for tables. For non-table objects, an
+						 * error will be thrown later via
+						 * preprocess_pubobj_list().
+						 */
+						$$->pubtable = makeNode(PublicationTable);
+						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
+						$$->pubtable->exclude_columns = $3;
+						$$->pubtable->whereClause = $4;
+					}
+					else
+					{
+						$$->name = $1;
+					}
+					$$->location = @1;
+				}
 			| ColId indirection opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10729,6 +10770,16 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| ColId indirection EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
+					$$->pubtable->exclude_columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->location = @1;
+				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
 			| extended_relation_expr opt_column_list OptWhereClause
 				{
@@ -10739,6 +10790,15 @@ PublicationObjSpec:
 					$$->pubtable->columns = $2;
 					$$->pubtable->whereClause = $3;
 				}
+			| extended_relation_expr EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $1;
+					$$->pubtable->exclude_columns = $3;
+					$$->pubtable->whereClause = $4;
+				}
 			| CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 5512b4cba7f..f36c361abd5 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,9 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/* Indicate if no column is included in the publication */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1099,6 +1102,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	bool		first = true;
 	Relation	relation = RelationIdGetRelation(entry->publish_as_relid);
 	bool		found_pub_collist = false;
+	bool		found_pub_exclude_collist = false;
 	Bitmapset  *relcols = NULL;
 
 	pgoutput_ensure_entry_cxt(data, entry);
@@ -1120,12 +1124,32 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		Bitmapset  *excludecols = NULL;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
 														 entry->entry_cxt, &cols);
 
+		/* Retrieve the bitmap of exclude columns for the publication. */
+		found_pub_exclude_collist |= check_and_fetch_exclude_column_list(pub,
+																		 entry->publish_as_relid,
+																		 entry->entry_cxt, &excludecols);
+
+		/*
+		 * cols and exclude cols can't appear together. Syntax for it is not
+		 * supported. If column list is not present check for excluded column
+		 * list and construct a corresponding column list.
+		 */
+		if (!cols && found_pub_exclude_collist)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, excludecols);
+			MemoryContextSwitchTo(oldcxt);
+		}
+
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
 		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
@@ -1144,7 +1168,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1155,8 +1179,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		{
 			entry->columns = cols;
 			first = false;
+
+			if (excludecols && !cols)
+				entry->no_cols_published = true;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published && cols) || !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1165,10 +1192,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	}							/* loop all subscribed publications */
 
 	/*
-	 * If no column list publications exist, columns to be published will be
-	 * computed later according to the 'publish_generated_columns' parameter.
+	 * If no column list or excluded column list publications exist, columns
+	 * to be published will be computed later according to the
+	 * 'publish_generated_columns' parameter.
 	 */
-	if (!found_pub_collist)
+	if (!found_pub_collist && !found_pub_exclude_collist)
 		entry->columns = NULL;
 
 	RelationClose(relation);
@@ -1480,6 +1508,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table is present in the exclude column list. Skip
+	 * publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2057,6 +2092,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2106,6 +2142,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6cc55afd498..e918c8b43b7 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4706,6 +4706,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelqual;
 	int			i_prattrs;
 	int			i_prexcept;
+	int			i_prexcludeattrs;
 	int			i,
 				j,
 				ntups;
@@ -4723,7 +4724,15 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 		/* FIXME: 180000 should be changed to 190000 later for PG19. */
 		if (fout->remoteVersion >= 180000)
-			appendPQExpBufferStr(query, " prexcept,\n");
+			appendPQExpBufferStr(query, " prexcept, "
+								 "(CASE\n"
+								 "  WHEN pr.prexcludeattrs IS NOT NULL THEN\n"
+								 "    (SELECT array_agg(attname)\n"
+								 "       FROM\n"
+								 "         pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prexcludeattrs::pg_catalog.int2[], 1)) s,\n"
+								 "         pg_catalog.pg_attribute\n"
+								 "      WHERE attrelid = pr.prrelid AND attnum = prexcludeattrs[s])\n"
+								 "  ELSE NULL END) prexcludeattrs, \n");
 		else
 			appendPQExpBufferStr(query, " false AS prexcept,\n");
 
@@ -4755,6 +4764,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
 	i_prexcept = PQfnumber(res, "prexcept");
+	i_prexcludeattrs = PQfnumber(res, "prexcludeattrs");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4822,6 +4832,30 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		if (!PQgetisnull(res, i, i_prexcludeattrs))
+		{
+			char	  **attnames;
+			int			nattnames;
+			PQExpBuffer excludeattribs;
+
+			if (!parsePGArray(PQgetvalue(res, i, i_prexcludeattrs),
+							  &attnames, &nattnames))
+				pg_fatal("could not parse %s array", "prattrs");
+			excludeattribs = createPQExpBuffer();
+			for (int k = 0; k < nattnames; k++)
+			{
+				if (k > 0)
+					appendPQExpBufferStr(excludeattribs, ", ");
+
+				appendPQExpBufferStr(excludeattribs, fmtId(attnames[k]));
+			}
+			pubrinfo[j].pubrexcludeattrs = excludeattribs->data;
+			free(excludeattribs);	/* but not excludeattribs->data */
+			free(attnames);
+		}
+		else
+			pubrinfo[j].pubrexcludeattrs = NULL;
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
@@ -4907,6 +4941,9 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	if (pubrinfo->pubrattrs)
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
 
+	if (pubrinfo->pubrexcludeattrs)
+		appendPQExpBuffer(query, " EXCEPT (%s)", pubrinfo->pubrexcludeattrs);
+
 	if (pubrinfo->pubrelqual)
 	{
 		/*
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 096f29346d8..e01c2d1afbd 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -681,6 +681,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	char	   *pubrexcludeattrs;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 10b5f7f29cb..75b6c20157b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,14 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "	  , NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,37 +3040,64 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "     , (CASE WHEN pr.prexcludeattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prexcludeattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prexcludeattrs[s])\n"
+								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
-
-				appendPQExpBuffer(&buf,
+								  "WHERE pr.prrelid = '%s'\n"
+								  "AND NOT pr.prexcept\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "		, NULL\n"
-								  "		, NULL\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3109,6 +3138,11 @@ describeOneTableDetails(const char *schemaname,
 					appendPQExpBuffer(&buf, " (%s)",
 									  PQgetvalue(result, i, 2));
 
+				/* exclude column list (if any) */
+				if (!PQgetisnull(result, i, 3))
+					appendPQExpBuffer(&buf, " EXCEPT (%s)",
+									  PQgetvalue(result, i, 3));
+
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
 					appendPQExpBuffer(&buf, " WHERE %s",
@@ -6525,6 +6559,9 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 			if (!PQgetisnull(res, i, 3))
 				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
 
+			if (!PQgetisnull(res, i, 4))
+				appendPQExpBuffer(buf, " EXCEPT (%s)", PQgetvalue(res, i, 4));
+
 			if (!PQgetisnull(res, i, 2))
 				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
 		}
@@ -6706,6 +6743,21 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBufferStr(&buf,
+									 ", (CASE WHEN pr.prexcludeattrs IS NOT NULL THEN\n"
+									 "     pg_catalog.array_to_string("
+									 "      ARRAY(SELECT attname\n"
+									 "              FROM\n"
+									 "                pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prexcludeattrs::pg_catalog.int2[], 1)) s,\n"
+									 "                pg_catalog.pg_attribute\n"
+									 "        WHERE attrelid = c.oid AND attnum = prexcludeattrs[s]), ', ')\n"
+									 "       ELSE NULL END)");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 33b771990bd..5344559c88e 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -140,6 +140,7 @@ typedef struct PublicationRelInfo
 	Node	   *whereClause;
 	List	   *columns;
 	bool		except;
+	List	   *exclude_columns;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
@@ -181,15 +182,21 @@ extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
 										MemoryContext mcxt, Bitmapset **cols);
+extern bool check_and_fetch_exclude_column_list(Publication *pub, Oid relid,
+												MemoryContext mcxt,
+												Bitmapset **cols);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
+extern Bitmapset *pub_exclude_collist_validate(Relation targetrel,
+											   List *exclude_columns);
 extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 											bool if_not_exists);
 
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *excludecols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..4c1b4ddbddc 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -36,6 +36,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
 	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prexcludeattrs; /* columns to exclude */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d901cb0ffa7..14861d180ab 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4236,6 +4236,7 @@ typedef struct PublicationTable
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
 	bool		except;			/* exclude the relation */
+	List	   *exclude_columns;	/* List of columns to be excluded */
 } PublicationTable;
 
 /*
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5d025328704..a274b3cff31 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,71 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+CREATE TABLE pub_test_except3 (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  syntax error at or near ";"
+LINE 1: ...BLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+                                                                      ^
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen1);
+ERROR:  cannot use generated column "gen1" in publication except column list
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {c,d}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  except column list must not be specified in ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
+DROP TABLE pub_test_except3;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index af31a2214ca..6b23f215739 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,51 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+CREATE TABLE pub_test_except3 (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen1);
+
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
+DROP TABLE pub_test_except3;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
index 1d115283809..ec77f2e8d04 100644
--- a/src/test/subscription/t/036_rep_changes_except_table.pl
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -1,7 +1,7 @@
 
 # Copyright (c) 2021-2022, PostgreSQL Global Development Group
 
-# Logical replication tests for except table publications
+# Logical replication tests for except table and except column publications
 use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
@@ -77,6 +77,64 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*), min(a), max(a) FROM public.tab1");
 is($result, qq(0||), 'check rows on subscriber catchup');
 
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2 EXCEPT (b, c)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+	'check that initial sync for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is($result, qq(1||), 'check that initial sync for except column publication');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+# Test incremental changes
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is( $result, qq(1||
+4||), 'check incremental insert for except column publication');
+
+# Test for update
+$node_publisher->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_subscriber->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET a = 3, b = 4, c = 5 WHERE a = 1");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|5|6
+|4|5),
+	'check update for except column publication');
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
-- 
2.34.1

v13-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v13-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 3b0a36848302e89d34a83f7a7d7291aa911b8293 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 19:08:35 +0530
Subject: [PATCH v13 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  16 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  97 ++++++++-
 src/test/regress/sql/publication.sql          |  47 ++++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++++
 25 files changed, 689 insertions(+), 134 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fa86c569dc4..4e37c928b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c32e6bc000d..3d0d29cf8b1 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2251,10 +2251,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 06452af9214..37e2c84bc10 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -89,8 +95,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -238,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..7fd8872db5f 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +458,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 570ef21d1fc..d9cd96dcaba 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..ec580e3b050 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -861,13 +877,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +903,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +925,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1181,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 159dc3781d0..5194b2fb6e2 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1246,6 +1251,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1284,6 +1310,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1596,6 +1695,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1808,6 +1921,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1880,6 +1994,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1955,8 +2070,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d813..8a8268a05d2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8624,7 +8624,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
@@ -18794,7 +18794,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e16f4832963..d7fe95a840f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -445,7 +445,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10612,7 +10613,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10632,12 +10633,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10675,6 +10677,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10750,6 +10753,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10762,6 +10784,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10788,6 +10812,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 693a766e6d7..5512b4cba7f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2063,7 +2063,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2174,22 +2175,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2208,7 +2193,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2223,6 +2209,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2301,6 +2289,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..bffdab2ab63 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5845,14 +5847,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5890,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5908,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8f0309e8fc..6cc55afd498 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -183,6 +183,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4510,8 +4512,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4677,6 +4705,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4688,8 +4717,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4700,6 +4738,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4715,6 +4754,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4726,6 +4766,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4739,7 +4780,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4780,6 +4825,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11544,6 +11592,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19783,6 +19834,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7417eab6aef..096f29346d8 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 0b0977788f1..56d6740b9ea 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c59..152fd7ff086 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3273,6 +3273,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..10b5f7f29cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6712,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6736,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 23cb27b4b05..0437628a6e2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2245,11 +2245,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3536,7 +3541,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..33b771990bd 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -163,7 +164,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +175,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 905b58e0279..d901cb0ffa7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4235,6 +4235,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4243,6 +4244,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index b2ffe0a8c20..5d025328704 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -209,13 +209,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -234,8 +258,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1963,17 +2027,20 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
-ERROR:  syntax error at or near "ALL"
-LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
-                                            ^
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
-(1 row)
+Tables from schemas:
+    "public"
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1984,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2001,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2039,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 15b2b1cfd28..af31a2214ca 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,23 +1238,39 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#82Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#81)
Re: Skipping schema changes in publication

On Thu, Jun 19, 2025 at 4:42 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
...

3.
TBH, I was wondering why a new catalog attribute was necessary...

Can't you simply re-use the existing attribute "prattrs" attribute.
e.g. let's just define negative means exclude.

e.g. a value of 1 3 means only the 1st and 3rd columns are published
e.g. a value of -1 -3 means all columns except 1st and 3rd columns are published
e.g. a value of null mean all columns are published

(mixes of negative and positive will not be possible)

Currently I have added a new attribute 'prexcludeattrs' in
pg_publication_rel table. I used this approach because it will be
easier for user to get the exclude column list, in code no extra
processing is required to get the exclude column list.

For an approach to use negative numbers for exclude columns. I see an
advantage that we do not need to introduce a new column for
pg_publication_rel. But in code, each time we want to get a column
list or exclude column list we need an extra processing of 'prattrs'
columns. Also I don't see any existing catalog table using a negative
attribute for column list.

Based on above observations, I feel that the current is better.

Please correct me if I missed an advantage for the approach you suggested.

OK. Maybe using negative numbers was a bridge too far...

But IMO it is not good to have 2 separate attributes for the lists.
Doing so implies they can coexist, but that is not true. I felt there
are not really 2 "kinds" of columns list anyway -- there is just a
"column list" which defines columns that are either included or
excluded from the publication determined by EXCEPT.

Having dual lists gets weird/confusing to describe them -- you end up
continually having to refer to the other one to clarify behaviour.

e.g. Does 'prattrs' value NULL mean publish everything? Well, no...
that depends if there is a non null 'prexcludeattrs'
e.g. Does 'prexcludeattrs' value NULL mean publish everything? Well,
no... that depends if there is a non null 'prattrs'

Furthermore, all the code is doubling up referring to "column list"
and "exclude column list" -- code / docs / comments / error messages.
There are quite a lot of places the patch touches that I thought were
not really needed if you don't have 2 different kinds of column-lists.

To summarise, I felt it would be better to just keep the existing
'prattrs' as the one-and-only column list, but add another BOOLEAN
attribute to flag whether 'prattrs' columns should be included or
excluded.

prattrs; prattrs_exclude; Means
--------------------------------------------
1 2 3 f only cols 1,2,3 will be published
4 5 6 t only cols 4,5,6 will NOT be published
null f all cols are published (flag is ignored)
null t all cols are published (flag is ignored)

5.
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);

Those tags don't seem correct. e.g. "users" and "security_pin" are not
<structname> (???).

Perhaps, every other example here is wrong too and you just copied
them? Anyway, something here looks wrong to me.

I saw different documents and usage of tags seems not well defined.
For example for table we are using tags in document
create_publication.sgml, update.sgml <structname> is used, in document
table.sgml, advanced.sgml <classname> is used, and in
logical-replication.sgml <literal> is used. Similarly for column
names <structname>, <structfield> or <literal> are used in different
parts of the document.

I kept the changed tag to <structfield> for the column for this patch.
Do you have any suggestions?

No, for this patch I think it is best that you just follow nearby code
(as you are already doing). I plan to raise another thread to ask what
are the guidelines for this sort of markup which is currently used
inconsistently in different places.

//////////

Below are a few more review comments for v13-0003

======
Commit message

1.
Typo /THe/The/

~~~

2.
The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (exclude_column_list)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (exclude_column_list)

~

I felt since you say these "For example:" it would be better to give
real examples.
e.g. say "(col1,col2,col3)" instead of "(exclude_column_list)".

~~~

3.
Typo /family of command/family of commands/

======
doc/src/sgml/logical-replication.sgml

4.
I am not sure that it was a good idea to be making a new term called
an "exclude column list"... because in introduces a new concept of
something that sounds like it is a different kind of list, and now you
have to keep referring everywhere to both to "column list" versus
"exclude column list". All the doubling up add more complication I
think.

IMO really there is just a "column list". Whether that list is for
exclusion or not just depends on the presence of EXCEPT. So I felt
maybe all places mentioning "exclude column list" could be rephrased.

======
src/backend/catalog/pg_publication.c

5.
+/*
+ * Returns true if the relation has exluded column list associated with the
+ * publication, false otherwise.
+ *
+ * If a exclude column list is found, the corresponding bitmap is returned
+ * through the cols parameter, if provided. The bitmap is constructed
within the
+ * given memory context (mcxt).
+ */
+

Typo /exluded column/an excluded column/
Typo /exclude column list/excluded column list/

~~~

6.
+/*
+ * pub_exclude_collist_validate
+ * Process and validate the 'excluded columns' list and ensure the columns
+ * are all valid to exclude from publication.  Checks for and raises an
+ * ERROR for any unknown columns, system columns, duplicate columns, or
+ * generated columns.
+ *

Why can't you exclude generated columns?

e.g. Maybe PUBLICATION says publish_generated_columns=stored and there
are 100s of such columns, but the user just wants to exclude one of
them. Why say they cannot do that? Hmm. Perhaps this is being already
handled elsewhere, in which case this comment still seems misleading.

======
src/backend/commands/publicationcmds.c

7.
+ * With REPLICA IDENTITY FULL, no column list and no excluded column
+ * list is allowed.

Really, just "no column list is allowed." same as it said before.

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

#83Peter Smith
smithpb2250@gmail.com
In reply to: Peter Smith (#82)
Re: Skipping schema changes in publication
5.
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);

Those tags don't seem correct. e.g. "users" and "security_pin" are not
<structname> (???).

Perhaps, every other example here is wrong too and you just copied
them? Anyway, something here looks wrong to me.

I saw different documents and usage of tags seems not well defined.
For example for table we are using tags in document
create_publication.sgml, update.sgml <structname> is used, in document
table.sgml, advanced.sgml <classname> is used, and in
logical-replication.sgml <literal> is used. Similarly for column
names <structname>, <structfield> or <literal> are used in different
parts of the document.

I kept the changed tag to <structfield> for the column for this patch.
Do you have any suggestions?

No, for this patch I think it is best that you just follow nearby code
(as you are already doing). I plan to raise another thread to ask what
are the guidelines for this sort of markup which is currently used
inconsistently in different places.

FYI - I created a new thread asking this markup question [1]/messages/by-id/CAHut+Pvtf24r+bdPgBind84dBLPvgNL7aB+=HxAUupdPuo2gRg@mail.gmail.com.

======
[1]: /messages/by-id/CAHut+Pvtf24r+bdPgBind84dBLPvgNL7aB+=HxAUupdPuo2gRg@mail.gmail.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#84Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#82)
3 attachment(s)
Re: Skipping schema changes in publication

On Fri, 20 Jun 2025 at 09:28, Peter Smith <smithpb2250@gmail.com> wrote:

On Thu, Jun 19, 2025 at 4:42 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
...

3.
TBH, I was wondering why a new catalog attribute was necessary...

Can't you simply re-use the existing attribute "prattrs" attribute.
e.g. let's just define negative means exclude.

e.g. a value of 1 3 means only the 1st and 3rd columns are published
e.g. a value of -1 -3 means all columns except 1st and 3rd columns are published
e.g. a value of null mean all columns are published

(mixes of negative and positive will not be possible)

Currently I have added a new attribute 'prexcludeattrs' in
pg_publication_rel table. I used this approach because it will be
easier for user to get the exclude column list, in code no extra
processing is required to get the exclude column list.

For an approach to use negative numbers for exclude columns. I see an
advantage that we do not need to introduce a new column for
pg_publication_rel. But in code, each time we want to get a column
list or exclude column list we need an extra processing of 'prattrs'
columns. Also I don't see any existing catalog table using a negative
attribute for column list.

Based on above observations, I feel that the current is better.

Please correct me if I missed an advantage for the approach you suggested.

OK. Maybe using negative numbers was a bridge too far...

But IMO it is not good to have 2 separate attributes for the lists.
Doing so implies they can coexist, but that is not true. I felt there
are not really 2 "kinds" of columns list anyway -- there is just a
"column list" which defines columns that are either included or
excluded from the publication determined by EXCEPT.

Having dual lists gets weird/confusing to describe them -- you end up
continually having to refer to the other one to clarify behaviour.

e.g. Does 'prattrs' value NULL mean publish everything? Well, no...
that depends if there is a non null 'prexcludeattrs'
e.g. Does 'prexcludeattrs' value NULL mean publish everything? Well,
no... that depends if there is a non null 'prattrs'

Furthermore, all the code is doubling up referring to "column list"
and "exclude column list" -- code / docs / comments / error messages.
There are quite a lot of places the patch touches that I thought were
not really needed if you don't have 2 different kinds of column-lists.

To summarise, I felt it would be better to just keep the existing
'prattrs' as the one-and-only column list, but add another BOOLEAN
attribute to flag whether 'prattrs' columns should be included or
excluded.

prattrs; prattrs_exclude; Means
--------------------------------------------
1 2 3 f only cols 1,2,3 will be published
4 5 6 t only cols 4,5,6 will NOT be published
null f all cols are published (flag is ignored)
null t all cols are published (flag is ignored)

I agree with your point and also it would be a better approach. In
patch 0001 an column 'prexcept' was added in pg_publication_rel. We
use that only for publication with all tables. I have reused this
column for patch 0003. If publication is not for all tables and the
'prexcept' flag is true, it implies that the columns in 'prattrs' are
to be excluded from being published. I have included the changes for
it in v14-0003 patch.

5.
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);

Those tags don't seem correct. e.g. "users" and "security_pin" are not
<structname> (???).

Perhaps, every other example here is wrong too and you just copied
them? Anyway, something here looks wrong to me.

I saw different documents and usage of tags seems not well defined.
For example for table we are using tags in document
create_publication.sgml, update.sgml <structname> is used, in document
table.sgml, advanced.sgml <classname> is used, and in
logical-replication.sgml <literal> is used. Similarly for column
names <structname>, <structfield> or <literal> are used in different
parts of the document.

I kept the changed tag to <structfield> for the column for this patch.
Do you have any suggestions?

No, for this patch I think it is best that you just follow nearby code
(as you are already doing). I plan to raise another thread to ask what
are the guidelines for this sort of markup which is currently used
inconsistently in different places.

Thanks for starting a thread for it.

//////////

Below are a few more review comments for v13-0003

======
Commit message

1.
Typo /THe/The/

~~~

Fixed

2.
The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (exclude_column_list)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (exclude_column_list)

~

I felt since you say these "For example:" it would be better to give
real examples.
e.g. say "(col1,col2,col3)" instead of "(exclude_column_list)".

Fixed

~~~

3.
Typo /family of command/family of commands/

======
doc/src/sgml/logical-replication.sgml

4.
I am not sure that it was a good idea to be making a new term called
an "exclude column list"... because in introduces a new concept of
something that sounds like it is a different kind of list, and now you
have to keep referring everywhere to both to "column list" versus
"exclude column list". All the doubling up add more complication I
think.

IMO really there is just a "column list". Whether that list is for
exclusion or not just depends on the presence of EXCEPT. So I felt
maybe all places mentioning "exclude column list" could be rephrased.

======
src/backend/catalog/pg_publication.c

5.
+/*
+ * Returns true if the relation has exluded column list associated with the
+ * publication, false otherwise.
+ *
+ * If a exclude column list is found, the corresponding bitmap is returned
+ * through the cols parameter, if provided. The bitmap is constructed
within the
+ * given memory context (mcxt).
+ */
+

Typo /exluded column/an excluded column/
Typo /exclude column list/excluded column list/

updated the comment according to latest implementation

~~~

6.
+/*
+ * pub_exclude_collist_validate
+ * Process and validate the 'excluded columns' list and ensure the columns
+ * are all valid to exclude from publication.  Checks for and raises an
+ * ERROR for any unknown columns, system columns, duplicate columns, or
+ * generated columns.
+ *

Why can't you exclude generated columns?

e.g. Maybe PUBLICATION says publish_generated_columns=stored and there
are 100s of such columns, but the user just wants to exclude one of
them. Why say they cannot do that? Hmm. Perhaps this is being already
handled elsewhere, in which case this comment still seems misleading.

I have removed this restriction. Now we can specify stored generated
columns in EXCEPT (column_list) when we use the
'publish_generated_columns' flag.

======
src/backend/commands/publicationcmds.c

7.
+ * With REPLICA IDENTITY FULL, no column list and no excluded column
+ * list is allowed.

Really, just "no column list is allowed." same as it said before.

======

Fixed

Thanks and Regards,
Shlok Kyal

Attachments:

v14-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v14-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 85ae9bd9a095a7d9d63044aebaa7057422c013f1 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v14 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 +++++--
 src/backend/commands/publicationcmds.c    | 111 ++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 120 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 ++++++++++
 7 files changed, 321 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..06452af9214 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +245,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..159dc3781d0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,94 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, false);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+		if (!OidIsValid(prid))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("relation \"%s\" is not part of the publication",
+							get_rel_name(relid))));
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1598,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 50f53159d58..e16f4832963 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10760,6 +10760,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10806,6 +10808,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 2c0b4f28c14..23cb27b4b05 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2242,7 +2242,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ba12678d1cb..905b58e0279 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4272,6 +4272,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..b2ffe0a8c20 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,126 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
+                                            ^
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..15b2b1cfd28 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v14-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v14-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 176ad491a5ea6d7eee8220a7e9810503445d8443 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 24 Jun 2025 01:35:55 +0530
Subject: [PATCH v14 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

Column list specifed with EXCEPT is stored in column "prattrs" in table
"pg_publication_rel" and also column "prexcept" is set to "true", to maintain
the column list that user wants to exclude from the publication.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |  5 +-
 doc/src/sgml/logical-replication.sgml         | 97 ++++++++++++++-----
 doc/src/sgml/ref/alter_publication.sgml       | 10 +-
 doc/src/sgml/ref/create_publication.sgml      | 17 +++-
 src/backend/catalog/pg_publication.c          | 92 +++++++++++++++---
 src/backend/commands/publicationcmds.c        | 35 +++++--
 src/backend/parser/gram.y                     | 65 +++++++++++++
 src/backend/replication/pgoutput/pgoutput.c   | 52 ++++++++--
 src/bin/pg_dump/pg_dump.c                     | 16 ++-
 src/bin/pg_dump/pg_dump.h                     |  1 +
 src/bin/psql/describe.c                       | 85 +++++++++++-----
 src/include/catalog/pg_publication.h          | 10 +-
 src/include/catalog/pg_publication_rel.h      |  5 +-
 src/test/regress/expected/publication.out     | 67 +++++++++++++
 src/test/regress/sql/publication.sql          | 46 +++++++++
 .../t/036_rep_changes_except_table.pl         | 94 +++++++++++++++++-
 16 files changed, 607 insertions(+), 90 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4e37c928b44..5bbd95aaebb 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the relation or column list must be excluded. If publication is
+       created <literal>FOR ALL TABLES</literal> and it is specified as true,
+       the relation should be excluded. Else if it is true the columns in
+       <literal>prattrs</literal> should be excluded from being published.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 3d0d29cf8b1..2d01e679691 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1340,10 +1340,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. On the subscriber side, the table
+   must include at least all the columns that are published. If no column list
+   is provided, all columns from the publisher are replicated by default.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1356,9 +1356,11 @@ Publications:
   </para>
 
   <para>
-   If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   If no column list or a column list with EXCEPT is specified, any columns
+   added to the table later are automatically replicated. This means that having
+   a column list which names all columns is not the same as having no
+   column list at all. If an column list is specified, any columns added to the
+   table later are automatically replicated.
   </para>
 
   <para>
@@ -1370,8 +1372,13 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
-   <literal>publish_generated_columns</literal></link>. See
-   <xref linkend="logical-replication-gencols"/> for details.
+   <literal>publish_generated_columns</literal></link>. Generated columns can
+   be included in column list specified with EXCEPT clause if publication
+   parameter
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> is not set to
+   <literal>none</literal>. Specified generated columns will not be published.
+   See <xref linkend="logical-replication-gencols"/> for details.
   </para>
 
   <para>
@@ -1391,11 +1398,13 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with EXCEPT clause
+   must not include the table's replica identity columns (see
    <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with EXCEPT clause may include replica identity columns.
   </para>
 
   <para>
@@ -1440,18 +1449,20 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal>, <literal>t2</literal> to be used in the
+    following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal> and a column list is defined for table
+    <literal>t2</literal> with EXCEPT clause to reduce the number of columns that will be
+    replicated. Notice that the order of column names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1459,12 +1470,13 @@ Publications:
      for each publication.
 <programlisting>
 /* pub # */ \dRp+
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+ Owner  | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
+--------+------------+---------+---------+---------+-----------+-------------------+----------
+ ubuntu | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1485,23 +1497,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1513,6 +1543,16 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
@@ -1526,6 +1566,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 37e2c84bc10..10b249710bc 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 7fd8872db5f..adee7d6d3d1 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -103,6 +103,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
       lists.
      </para>
 
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -474,6 +480,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ec580e3b050..e4591cb0b35 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,13 +263,17 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
+	bool		except = false;
 
 	if (pub->alltables)
 		return false;
@@ -296,6 +300,15 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* except found? */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+			except = DatumGetBool(cfdatum);
+
+		*except_columns = except && !pub->alltables;
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -479,7 +492,9 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
-	attnums = pub_collist_validate(pri->relation, pri->columns);
+	attnums = pub_collist_validate(pri->relation, pri->columns,
+								   pri->except && !pub->alltables,
+								   pub->pubgencols_type);
 
 	/* Form a tuple. */
 	memset(values, 0, sizeof(values));
@@ -568,7 +583,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
  * corresponding attnums.
  */
 Bitmapset *
-pub_collist_validate(Relation targetrel, List *columns)
+pub_collist_validate(Relation targetrel, List *columns, bool except_columns,
+					 PublishGencolsType pubgencols_type)
 {
 	Bitmapset  *set = NULL;
 	ListCell   *lc;
@@ -591,6 +607,19 @@ pub_collist_validate(Relation targetrel, List *columns)
 					errmsg("cannot use system column \"%s\" in publication column list",
 						   colname));
 
+		/*
+		 * Check if column list specified with EXCEPT have any stored
+		 * generated column and 'publish_generated_columns' is not set to
+		 * 'stored'.
+		 */
+		if (except_columns &&
+			TupleDescAttr(tupdesc, attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_STORED &&
+			pubgencols_type != PUBLISH_GENCOLS_STORED)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot use stored generated column \"%s\" in publication column list specified with EXCEPT when \"%s\" set to \"%s\"",
+						   colname, "publish_generated_columns", "stored"));
+
 		if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 			ereport(ERROR,
 					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
@@ -646,10 +675,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the excludecols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *excludecols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -672,6 +703,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (excludecols && bms_is_member(att->attnum, excludecols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -776,9 +810,14 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		HeapTuple	pubtup = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+		bool		is_table_excluded = ((Form_pg_publication) GETSTRUCT(pubtup))->puballtables &&
+			((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept;
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_table_excluded)
 			result = lappend_oid(result, pubid);
+
+		ReleaseSysCache(pubtup);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -1263,6 +1302,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Datum		columnsDatum;
+		Datum		exceptDatum;
+		Bitmapset  *columns = NULL;
+		bool		except = false;
+		bool		isnull;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1287,10 +1331,22 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
-			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
-										Anum_pg_publication_rel_prattrs,
-										&(nulls[2]));
+			exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+										  Anum_pg_publication_rel_prexcept,
+										  &isnull);
+
+			if (!isnull)
+				except = DatumGetBool(exceptDatum);
+
+			columnsDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+										   Anum_pg_publication_rel_prattrs,
+										   &(nulls[2]));
+
+			/* if column list is specified with EXCEPT */
+			if (!pub->alltables && except)
+				columns = pub_collist_to_bitmapset(NULL, columnsDatum, NULL);
+			else
+				values[2] = columnsDatum;
 
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
@@ -1303,8 +1359,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if ((!pub->alltables && except) || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1335,6 +1395,14 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if ((!pub->alltables && except) &&
+					bms_is_member(att->attnum, columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 5194b2fb6e2..8513e579df8 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,7 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ * 	  If any column is missing, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -381,6 +381,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +405,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +496,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1443,6 +1451,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1458,6 +1467,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		exceptDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1474,6 +1484,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull)
+					oldexcept = DatumGetBool(exceptDatum);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1493,7 +1510,9 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * is cheap enough that that seems harmless.
 				 */
 				newcolumns = pub_collist_validate(newpubrel->relation,
-												  newpubrel->columns);
+												  newpubrel->columns,
+												  newpubrel->except && !pubform->puballtables,
+												  pubform->pubgencols);
 
 				/*
 				 * Check if any of the new set of relations matches with the
@@ -1505,7 +1524,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
@@ -1522,6 +1542,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7fe95a840f..64827dc1b09 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,6 +446,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list except_pub_obj_list
+				opt_exclude_column_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -4413,6 +4414,10 @@ opt_column_list:
 			| /*EMPTY*/								{ $$ = NIL; }
 		;
 
+opt_exclude_column_list:
+			'(' columnList ')'						{ $$ = $2; }
+		;
+
 columnList:
 			columnElem								{ $$ = list_make1($1); }
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
@@ -10679,6 +10684,17 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| TABLE relation_expr EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $2;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->pubtable->except = true;
+					$$->location = @1;
+				}
 			| TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10719,6 +10735,34 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
+			| ColId EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					/*
+					 * If either a row filter or exclude column list is
+					 * specified, create a PublicationTable object.
+					 */
+					if ($3 || $4)
+					{
+						/*
+						 * The OptWhereClause must be stored here but it is
+						 * valid only for tables. For non-table objects, an
+						 * error will be thrown later via
+						 * preprocess_pubobj_list().
+						 */
+						$$->pubtable = makeNode(PublicationTable);
+						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
+						$$->pubtable->except = true;
+					}
+					else
+					{
+						$$->name = $1;
+					}
+					$$->location = @1;
+				}
 			| ColId indirection opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10729,6 +10773,17 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| ColId indirection EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->pubtable->except = true;
+					$$->location = @1;
+				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
 			| extended_relation_expr opt_column_list OptWhereClause
 				{
@@ -10739,6 +10794,16 @@ PublicationObjSpec:
 					$$->pubtable->columns = $2;
 					$$->pubtable->whereClause = $3;
 				}
+			| extended_relation_expr EXCEPT opt_exclude_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $1;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = true;
+				}
 			| CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 5512b4cba7f..be9f548007d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,9 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/* Indicate if no column is included in the publication */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1066,12 +1069,19 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		found = false;
+		bool		except_columns = false;
+
+		found = check_and_fetch_column_list(pub, entry->publish_as_relid, NULL,
+											NULL, &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (found && !except_columns)
 			continue;
 
 		if (first)
@@ -1120,11 +1130,30 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		no_col_published = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				no_col_published = true;
+		}
 
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
@@ -1132,7 +1161,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		 * of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!no_col_published && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1144,7 +1173,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1154,9 +1183,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->no_cols_published = no_col_published;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published != no_col_published) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1480,6 +1511,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table is present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2057,6 +2095,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2106,6 +2145,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6cc55afd498..25f9161f35e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4780,10 +4780,10 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
+		if (strcmp(prexcept, "t") == 0 && PQgetisnull(res, i, i_prattrs))
 			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
 
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
@@ -4797,6 +4797,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			pubrinfo[j].pubrelqual = NULL;
 		else
 			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4825,7 +4826,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (strcmp(prexcept, "t") == 0 && PQgetisnull(res, i, i_prattrs))
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4905,7 +4906,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
-		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBuffer(query, " EXCEPT (%s)", pubrinfo->pubrattrs);
+		else
+			appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 096f29346d8..daf6fb51a3f 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -681,6 +681,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 10b5f7f29cb..a55c4c6505d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,14 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,37 +3040,61 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
-
-				appendPQExpBuffer(&buf,
+								  "WHERE pr.prrelid = '%s' "
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3106,8 +3132,15 @@ describeOneTableDetails(const char *schemaname,
 
 				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
+				{
+					if (!PQgetisnull(result, i, 3) &&
+						strcmp(PQgetvalue(result, i, 3), "t") == 0)
+						appendPQExpBuffer(&buf, " EXCEPT (%s)",
+										  PQgetvalue(result, i, 2));
+					else
+						appendPQExpBuffer(&buf, " (%s)",
+										  PQgetvalue(result, i, 2));
+				}
 
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 33b771990bd..9ac94159c13 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -180,16 +180,20 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
-extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
+extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns,
+									   bool except_columns,
+									   PublishGencolsType pubgencols_type);
 extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 											bool if_not_exists);
 
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *excludecols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5d025328704..833c19305a9 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,73 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+CREATE TABLE pub_test_except3 (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED, gen2 int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  syntax error at or near ";"
+LINE 1: ...BLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+                                                                      ^
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen1);
+ERROR:  cannot use stored generated column "gen1" in publication column list specified with EXCEPT when "publish_generated_columns" set to "stored"
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen2) with (publish_generated_columns);
+ERROR:  cannot use virtual generated column "gen2" in publication column list
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {c,d}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
+DROP TABLE pub_test_except3;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index af31a2214ca..3dbbddb1950 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,52 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+CREATE TABLE pub_test_except3 (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED, gen2 int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen1);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except3 EXCEPT (gen2) with (publish_generated_columns);
+
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
+DROP TABLE pub_test_except3;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
index 1d115283809..aefa67e6145 100644
--- a/src/test/subscription/t/036_rep_changes_except_table.pl
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -1,7 +1,7 @@
 
 # Copyright (c) 2021-2022, PostgreSQL Global Development Group
 
-# Logical replication tests for except table publications
+# Logical replication tests for except table and except column publications
 use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
@@ -77,6 +77,98 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*), min(a), max(a) FROM public.tab1");
 is($result, qq(0||), 'check rows on subscriber catchup');
 
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2 EXCEPT (b, c)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+	'check that initial sync for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is($result, qq(1||), 'check that initial sync for except column publication');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+# Test incremental changes
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is( $result, qq(1||
+4||), 'check incremental insert for except column publication');
+
+# Test for update
+$node_publisher->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_subscriber->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET a = 3, b = 4, c = 5 WHERE a = 1");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|5|6
+|4|5),
+	'check update for except column publication');
+
+# Test ALTER PUBLICATION for EXCEPT (col_list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab3 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3");
+is($result, qq(1||3), 'check alter publication with EXCEPT');
+
+# Test for publication created on table with generated columns and column list
+# specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET (publish_generated_columns)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4");
+is( $result, qq(1||3
+2||6), 'check publication with generated columns and EXCEPT');
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
-- 
2.34.1

v14-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v14-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 3b0a36848302e89d34a83f7a7d7291aa911b8293 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 19:08:35 +0530
Subject: [PATCH v14 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/create_publication.sgml      |  29 ++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 ++++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  16 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  97 ++++++++-
 src/test/regress/sql/publication.sql          |  47 ++++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++++
 25 files changed, 689 insertions(+), 134 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fa86c569dc4..4e37c928b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c32e6bc000d..3d0d29cf8b1 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2251,10 +2251,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 06452af9214..37e2c84bc10 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -89,8 +95,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -238,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..7fd8872db5f 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +458,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 570ef21d1fc..d9cd96dcaba 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..ec580e3b050 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -861,13 +877,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +903,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +925,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1181,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 159dc3781d0..5194b2fb6e2 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1246,6 +1251,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1284,6 +1310,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1596,6 +1695,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1808,6 +1921,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1880,6 +1994,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1955,8 +2070,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d813..8a8268a05d2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8624,7 +8624,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
@@ -18794,7 +18794,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e16f4832963..d7fe95a840f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -445,7 +445,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10612,7 +10613,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10632,12 +10633,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10675,6 +10677,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10750,6 +10753,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10762,6 +10784,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10788,6 +10812,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 693a766e6d7..5512b4cba7f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2063,7 +2063,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2174,22 +2175,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2208,7 +2193,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2223,6 +2209,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2301,6 +2289,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..bffdab2ab63 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5845,14 +5847,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5890,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5908,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8f0309e8fc..6cc55afd498 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -183,6 +183,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4510,8 +4512,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4677,6 +4705,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4688,8 +4717,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4700,6 +4738,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4715,6 +4754,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4726,6 +4766,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4739,7 +4780,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4780,6 +4825,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11544,6 +11592,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19783,6 +19834,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7417eab6aef..096f29346d8 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 0b0977788f1..56d6740b9ea 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c59..152fd7ff086 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3273,6 +3273,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..10b5f7f29cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6712,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6736,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 23cb27b4b05..0437628a6e2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2245,11 +2245,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3536,7 +3541,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..33b771990bd 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -163,7 +164,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +175,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 905b58e0279..d901cb0ffa7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4235,6 +4235,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4243,6 +4244,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index b2ffe0a8c20..5d025328704 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -209,13 +209,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -234,8 +258,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1963,17 +2027,20 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
-ERROR:  syntax error at or near "ALL"
-LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...
-                                            ^
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
           Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
-(1 row)
+Tables from schemas:
+    "public"
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1984,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2001,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2039,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 15b2b1cfd28..af31a2214ca 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,23 +1238,39 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
-ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
 
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#85Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#84)
Re: Skipping schema changes in publication

Hi Shlok.

Below are some review comments for v14-0003

======
1. GENERAL

Since the new syntax uses EXCEPT, then, in my opinion, you should try
to use that same term where possible when describing things. I
understand it is hard to do this in text and I agree often it makes
more sense to say "exclude" columns etc, but OTOH in the code there
are lots of places where you could have named vars/params differently:
e.g. 'except_collist' instead of 'exclude_collist' might have been
better.

======
Commit message

2.
Column list specifed with EXCEPT is stored in column "prattrs" in table
"pg_publication_rel" and also column "prexcept" is set to "true", to maintain
the column list that user wants to exclude from the publication.

~

That paragraph could do with some rewording. For example, AFAIK,
"prattrs" is for all column lists -- not just except col-lists, but
the way it is described here sounds different.

Also, /specifed/specified/

======
doc/src/sgml/catalogs.sgml

3. (52.42. pg_publication_rel)

       <para>
-       True if the relation must be excluded
+       True if the relation or column list must be excluded. If publication is
+       created <literal>FOR ALL TABLES</literal> and it is specified as true,
+       the relation should be excluded. Else if it is true the columns in
+       <literal>prattrs</literal> should be excluded from being published.
       </para></entry>

I felt this could be expressed more simply without mentioning anything
about FOR ALL TABLES.

SUGGESTION
True if the column list or relation must be excluded from publication.
If a column list is specified in <literal>prattrs</literal>, then
exclude only those columns. If <literal>prattrs</literal> is NULL,
then exclude the entire relation.

======
doc/src/sgml/logical-replication.sgml

4. (29.5. Column Lists)

   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each
table should be
+   replicated or excluded from replication. On the subscriber side, the table
+   must include at least all the columns that are published. If no column list
+   is provided, all columns from the publisher are replicated by default.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>

I felt this patch may have changed too much text. IMO, you only needed
to say "... are replicated or excluded from replication.". The other
changes did not seem necessary.

~~~

5.
   <para>
-   If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   If no column list or a column list with EXCEPT is specified, any columns
+   added to the table later are automatically replicated. This means
that having
+   a column list which names all columns is not the same as having no
+   column list at all. If an column list is specified, any columns added to the
+   table later are automatically replicated.
   </para>

5a.
"This means that having a column list which names all columns is not
the same as having no column list at all." -- That note does not make
sense when you say EXCEPT. I think some rewording is needed here.

~

5b.
"If an column list is specified, any columns added to the table later
are automatically replicated.".

This made no sense -- some words missing?

~~~

6.
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
-   <literal>publish_generated_columns</literal></link>. See
-   <xref linkend="logical-replication-gencols"/> for details.
+   <literal>publish_generated_columns</literal></link>. Generated columns can
+   be included in column list specified with EXCEPT clause if publication
+   parameter
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> is not set to
+   <literal>none</literal>. Specified generated columns will not be published.
+   See <xref linkend="logical-replication-gencols"/> for details.
   </para>

I am not so sure about this. It seemed overly strict to me.

Why can't it simply say:
"Generated columns can also be specified in a column list. This allows
generated columns to be published or excluded, regardless of the
publication parameter..."

Specifically, I don't know why you need to say:
Generated columns can be included in column list specified with EXCEPT
clause if publication parameter publish_generated_columns is not set
to none. Specified generated columns will not be published.

IIUC, then EXCEPT (gencol1, gencol2) is saying to exclude the named
cols. So if param is "stored", then the named cols will be excluded.
OTOH, if param is "none" then all generated cols will be excluded
anyway, so why not just allow the EXCEPT (gencol,gencol2) here as
well, because the result will be the same.

~~~

7. (29.5.1. Examples)

    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal>, <literal>t2</literal> to be
used in the
+    following example.

/Create tables t1, t2/Create tables t1 and t2/

~~~

8.
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal> and a column list is defined for table
+    <literal>t2</literal> with EXCEPT clause to reduce the number of
columns that will be
+    replicated. Notice that the order of column names in the column
lists does not matter.

BEFORE
A column list is defined for table t1 and a column list is defined for
table t2...

SUGGESTION (added comma, etc.)
A column list is defined for table t1, and another column list is
defined for table t2...

~~~

9.
The final example still says:
"Only data from the column list of publication p1 is replicated."

That doesn't seem quite appropriate now that you also have an EXCEPT
column list.

SUGGESTION:
Only data specified by the column lists of publication p1 is replicated.

======
doc/src/sgml/ref/create_publication.sgml

10.
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>

I felt that to be clearer the preceding paragraph should be changed as follows:

/When a column list is specified, only the named columns are
replicated./When a column list without EXCEPT is specified, only the
named columns are replicated./

~~~

11. CREATE PUBLICATION (NOTES section)

11a.
The NOTES talk about replica identity columns -- should you mention EXCEPT here?

~

11b.
The NOTES talk about generated columns -- should you mention EXCEPT here?

======
src/backend/catalog/pg_publication.c

12. check_and_fetch_column_list

+ if (!isnull)
+ except = DatumGetBool(cfdatum);
+
+ *except_columns = except && !pub->alltables;

AFAICT, you can Assert(!pub->alltables) because you already checked
that earlier up front.
So you don't need 'except' var either. Just assign *except_cols up
front and then overwrite it later if true.

SUGGESTION:

*except_cols = false;

if (pub->alltables)
return false;
...
if (!isnull)
*except_cols = DatumGetBool(cfdatum);

~~~

13. publication_add_relation

  /* Validate and translate column names into a Bitmapset of attnums. */
- attnums = pub_collist_validate(pri->relation, pri->columns);
+ attnums = pub_collist_validate(pri->relation, pri->columns,
+    pri->except && !pub->alltables,
+    pub->pubgencols_type);

I am wondering why we are even calling a function to validate column
lists if pub->alltables was true. AFAIK, that combination of
column-lists and FOR ALL TABLES is not even possible, so the code
seems strange.

~~~

14. pub_exclude_collist_validate
.
+ /*
+ * Check if column list specified with EXCEPT have any stored
+ * generated column and 'publish_generated_columns' is not set to
+ * 'stored'.
+ */
+ if (except_columns &&
+ TupleDescAttr(tupdesc, attnum - 1)->attgenerated ==
ATTRIBUTE_GENERATED_STORED &&
+ pubgencols_type != PUBLISH_GENCOLS_STORED)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("cannot use stored generated column \"%s\" in publication
column list specified with EXCEPT when \"%s\" set to \"%s\"",
+    colname, "publish_generated_columns", "stored"));

As mentioned in the above DOCS comments, I was having doubts about why
we have this error.

If the parameter says "none", then generated columns will not be
replicated, so why should we care if the user also says
EXCEPT(gencol1,gencol2). Either way, the result will be the same; the
generated column will not be published.

~~~

15. GetRelationPublications

  {
  HeapTuple tup = &pubrellist->members[i]->tuple;
  Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+ HeapTuple pubtup = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+ bool is_table_excluded = ((Form_pg_publication)
GETSTRUCT(pubtup))->puballtables &&
+ ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept;
- if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+ if (except_flag == is_table_excluded)
  result = lappend_oid(result, pubid);
+
+ ReleaseS

I'm not 100% sure you need the additional 'pubtup'... Can't you just
look at the "prattrs" field to see if a column-list was specified? If
"prattrs" is null and "prexcept" is true, isn't that the same
combination as what you are looking for here?

~~~

16. pg_get_publication_tables

+ columnsDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+    Anum_pg_publication_rel_prattrs,
+    &(nulls[2]));
+
+ /* if column list is specified with EXCEPT */
+ if (!pub->alltables && except)
+ columns = pub_collist_to_bitmapset(NULL, columnsDatum, NULL);
+ else
+ values[2] = columnsDatum;

16a.
Something seems fishy here. Isn't there a pathway where you missed
assigning value[2] to anything?

~

16b.
Also, I feel there should be some other boolean variable used here
instead of checking bot (!pub->alltables && except) in multiple
places.

======
src/backend/replication/pgoutput/pgoutput.c

17. RelationSyncEntry
+
+ /* Indicate if no column is included in the publication */
+ bool no_cols_published;

Maybe this can have a more explanatory comment to explain why it is needed?

~~~

18. check_and_init_gencol

+ bool found = false;
+ bool except_columns = false;
+
+ found = check_and_fetch_column_list(pub, entry->publish_as_relid, NULL,
+ NULL, &except_columns);
+
  /*
  * The column list takes precedence over the
  * 'publish_generated_columns' parameter. Those will be checked later,
- * see pgoutput_column_list_init.
+ * see pgoutput_column_list_init. But when a column list is specified
+ * with EXCEPT, it should be checked.
  */
- if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+ if (found && !except_columns)
  continue;

The variable 'found' seems a poor name; how about 'has_column_list' or similar?

~~~

19. pgoutput_change

+ /*
+ * If all columns of a table is present in column list specified with
+ * EXCEPT, skip publishing the changes.
+ */
+ if (relentry->no_cols_published)
+ return;

/is present/are present/

======
src/bin/pg_dump/pg_dump.c

20. getPublicationTables

+ if (strcmp(prexcept, "t") == 0 && PQgetisnull(res, i, i_prattrs))
  pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+ else
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
  pubrinfo[j].dobj.catId.tableoid =
  atooid(PQgetvalue(res, i, i_tableoid));
@@ -4797,6 +4797,7 @@ getPublicationTables(Archive *fout, TableInfo
tblinfo[], int numTables)
  pubrinfo[j].pubrelqual = NULL;
  else
  pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+ pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);

Why not assign pubrinfo[j].pubexcept earlier so you don't have to
repeat the strcmp?

~~~

21.
- if (strcmp(prexcept, "t") == 0)
+ if (strcmp(prexcept, "t") == 0 && PQgetisnull(res, i, i_prattrs))
  simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);

Why not assign pubrinfo[j].pubexcept earlier so you don't have to
repeat the strcmp? Same also for the PQgetisnull(res, i,
i_prattrs))...

~~~

22. dumpPublicationTable

  if (pubrinfo->pubrattrs)
- appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+ {
+ if (pubrinfo->pubexcept)
+ appendPQExpBuffer(query, " EXCEPT (%s)", pubrinfo->pubrattrs);
+ else
+ appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+ }

SUGGESTION
{
if (pubrinfo->pubexcept)
appendPQExpBuffer(query, " EXCEPT");

appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
}

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

#86shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#84)
Re: Skipping schema changes in publication

On Tue, Jun 24, 2025 at 9:48 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have included the changes for
it in v14-0003 patch.

Thanks for the patches. I have reviewed patch001 alone, please find
few comments:

1)
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
   </para>

It is misleading, as far as I have understood, we do not drop the
tables or schemas associated with the pub; we just remove those from
the publication's object list. See previous doc:
"The ADD and DROP clauses will add and remove one or more
tables/schemas from the publication"

Perhaps we want to say the same thing when we speak about the 'drop'
aspect of RESET.

2)
AlterPublicationReset():

+ if (!OidIsValid(prid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("relation \"%s\" is not part of the publication",
+ get_rel_name(relid))));

Can you please help me understand which scenario will give this error?

Another question is do we really need this error? IIUC, we generally
give errors if a user has explicitly called out a name of an object
and that object is not found. Example:

postgres=# alter publication pubnew drop table t1,tab2;
ERROR: relation "t1" is not part of the publication

While in a few other cases, we pass missing_okay as true and do not
give errors. Please see other callers of performDeletion in
publicationcmds.c itself. There we have usage of missing_okay=true. I
have not researched myself, but please analyze the cases where
missing_okay is passed as true to figure out if those match our RESET
case. Try to reproduce if possible and then take a call.

3)
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...

There is a problem in syntax, I think the intention of testcase was to
run this query successfully.

thanks
Shveta

#87Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#85)
3 attachment(s)
Re: Skipping schema changes in publication

On Thu, 26 Jun 2025 at 09:06, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Below are some review comments for v14-0003

======
1. GENERAL

Since the new syntax uses EXCEPT, then, in my opinion, you should try
to use that same term where possible when describing things. I
understand it is hard to do this in text and I agree often it makes
more sense to say "exclude" columns etc, but OTOH in the code there
are lots of places where you could have named vars/params differently:
e.g. 'except_collist' instead of 'exclude_collist' might have been
better.

Fixed the variable names.

======
Commit message

2.
Column list specifed with EXCEPT is stored in column "prattrs" in table
"pg_publication_rel" and also column "prexcept" is set to "true", to maintain
the column list that user wants to exclude from the publication.

~

That paragraph could do with some rewording. For example, AFAIK,
"prattrs" is for all column lists -- not just except col-lists, but
the way it is described here sounds different.

Also, /specifed/specified/

Reworded the paragraph

======
doc/src/sgml/catalogs.sgml

3. (52.42. pg_publication_rel)

<para>
-       True if the relation must be excluded
+       True if the relation or column list must be excluded. If publication is
+       created <literal>FOR ALL TABLES</literal> and it is specified as true,
+       the relation should be excluded. Else if it is true the columns in
+       <literal>prattrs</literal> should be excluded from being published.
</para></entry>

I felt this could be expressed more simply without mentioning anything
about FOR ALL TABLES.

SUGGESTION
True if the column list or relation must be excluded from publication.
If a column list is specified in <literal>prattrs</literal>, then
exclude only those columns. If <literal>prattrs</literal> is NULL,
then exclude the entire relation.

Fixed

======
doc/src/sgml/logical-replication.sgml

4. (29.5. Column Lists)

<para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each
table should be
+   replicated or excluded from replication. On the subscriber side, the table
+   must include at least all the columns that are published. If no column list
+   is provided, all columns from the publisher are replicated by default.
See <xref linkend="sql-createpublication"/> for details on the syntax.
</para>

I felt this patch may have changed too much text. IMO, you only needed
to say "... are replicated or excluded from replication.". The other
changes did not seem necessary.

~~~

Fixed

5.
<para>
-   If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   If no column list or a column list with EXCEPT is specified, any columns
+   added to the table later are automatically replicated. This means
that having
+   a column list which names all columns is not the same as having no
+   column list at all. If an column list is specified, any columns added to the
+   table later are automatically replicated.
</para>

5a.
"This means that having a column list which names all columns is not
the same as having no column list at all." -- That note does not make
sense when you say EXCEPT. I think some rewording is needed here.

Fixed

~

5b.
"If an column list is specified, any columns added to the table later
are automatically replicated.".

This made no sense -- some words missing?

This change was done by mistake. Removed it.

~~~

6.
Generated columns can also be specified in a column list. This allows
generated columns to be published, regardless of the publication parameter
<link linkend="sql-createpublication-params-with-publish-generated-columns">
-   <literal>publish_generated_columns</literal></link>. See
-   <xref linkend="logical-replication-gencols"/> for details.
+   <literal>publish_generated_columns</literal></link>. Generated columns can
+   be included in column list specified with EXCEPT clause if publication
+   parameter
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> is not set to
+   <literal>none</literal>. Specified generated columns will not be published.
+   See <xref linkend="logical-replication-gencols"/> for details.
</para>

I am not so sure about this. It seemed overly strict to me.

Why can't it simply say:
"Generated columns can also be specified in a column list. This allows
generated columns to be published or excluded, regardless of the
publication parameter..."

Specifically, I don't know why you need to say:
Generated columns can be included in column list specified with EXCEPT
clause if publication parameter publish_generated_columns is not set
to none. Specified generated columns will not be published.

IIUC, then EXCEPT (gencol1, gencol2) is saying to exclude the named
cols. So if param is "stored", then the named cols will be excluded.
OTOH, if param is "none" then all generated cols will be excluded
anyway, so why not just allow the EXCEPT (gencol,gencol2) here as
well, because the result will be the same.

I have removed this change. And allowed specifying generated columns
in EXCEPT column list as well irrespective of value of
‘publish_generated_columns’.

~~~

7. (29.5.1. Examples)

<para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal>, <literal>t2</literal> to be
used in the
+    following example.

/Create tables t1, t2/Create tables t1 and t2/

Fixed

~~~

8.
<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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal> and a column list is defined for table
+    <literal>t2</literal> with EXCEPT clause to reduce the number of
columns that will be
+    replicated. Notice that the order of column names in the column
lists does not matter.

BEFORE
A column list is defined for table t1 and a column list is defined for
table t2...

SUGGESTION (added comma, etc.)
A column list is defined for table t1, and another column list is
defined for table t2...

Fixed

~~~

9.
The final example still says:
"Only data from the column list of publication p1 is replicated."

That doesn't seem quite appropriate now that you also have an EXCEPT
column list.

SUGGESTION:
Only data specified by the column lists of publication p1 is replicated.

Fixed

======
doc/src/sgml/ref/create_publication.sgml

10.
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>

I felt that to be clearer the preceding paragraph should be changed as follows:

/When a column list is specified, only the named columns are
replicated./When a column list without EXCEPT is specified, only the
named columns are replicated./

Fixed

~~~

11. CREATE PUBLICATION (NOTES section)

11a.
The NOTES talk about replica identity columns -- should you mention EXCEPT here?

Added notes for EXCEPT

~

11b.
The NOTES talk about generated columns -- should you mention EXCEPT here?

I felt it is not needed.

======
src/backend/catalog/pg_publication.c

12. check_and_fetch_column_list

+ if (!isnull)
+ except = DatumGetBool(cfdatum);
+
+ *except_columns = except && !pub->alltables;

AFAICT, you can Assert(!pub->alltables) because you already checked
that earlier up front.
So you don't need 'except' var either. Just assign *except_cols up
front and then overwrite it later if true.

SUGGESTION:

*except_cols = false;

if (pub->alltables)
return false;
...
if (!isnull)
*except_cols = DatumGetBool(cfdatum);

Fixed

~~~

13. publication_add_relation

/* Validate and translate column names into a Bitmapset of attnums. */
- attnums = pub_collist_validate(pri->relation, pri->columns);
+ attnums = pub_collist_validate(pri->relation, pri->columns,
+    pri->except && !pub->alltables,
+    pub->pubgencols_type);

I am wondering why we are even calling a function to validate column
lists if pub->alltables was true. AFAIK, that combination of
column-lists and FOR ALL TABLES is not even possible, so the code
seems strange.

Fixed

~~~

14. pub_exclude_collist_validate
.
+ /*
+ * Check if column list specified with EXCEPT have any stored
+ * generated column and 'publish_generated_columns' is not set to
+ * 'stored'.
+ */
+ if (except_columns &&
+ TupleDescAttr(tupdesc, attnum - 1)->attgenerated ==
ATTRIBUTE_GENERATED_STORED &&
+ pubgencols_type != PUBLISH_GENCOLS_STORED)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("cannot use stored generated column \"%s\" in publication
column list specified with EXCEPT when \"%s\" set to \"%s\"",
+    colname, "publish_generated_columns", "stored"));

As mentioned in the above DOCS comments, I was having doubts about why
we have this error.

If the parameter says "none", then generated columns will not be
replicated, so why should we care if the user also says
EXCEPT(gencol1,gencol2). Either way, the result will be the same; the
generated column will not be published.

Removed this restriction.

~~~

15. GetRelationPublications

{
HeapTuple tup = &pubrellist->members[i]->tuple;
Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+ HeapTuple pubtup = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+ bool is_table_excluded = ((Form_pg_publication)
GETSTRUCT(pubtup))->puballtables &&
+ ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept;
- if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+ if (except_flag == is_table_excluded)
result = lappend_oid(result, pubid);
+
+ ReleaseS

I'm not 100% sure you need the additional 'pubtup'... Can't you just
look at the "prattrs" field to see if a column-list was specified? If
"prattrs" is null and "prexcept" is true, isn't that the same
combination as what you are looking for here?

Yes, we can use this combination as well. Fixed it in latest patch.

~~~

16. pg_get_publication_tables

+ columnsDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+    Anum_pg_publication_rel_prattrs,
+    &(nulls[2]));
+
+ /* if column list is specified with EXCEPT */
+ if (!pub->alltables && except)
+ columns = pub_collist_to_bitmapset(NULL, columnsDatum, NULL);
+ else
+ values[2] = columnsDatum;

16a.
Something seems fishy here. Isn't there a pathway where you missed
assigning value[2] to anything?

Modified this change.

~

16b.
Also, I feel there should be some other boolean variable used here
instead of checking bot (!pub->alltables && except) in multiple
places.

Fixed

======
src/backend/replication/pgoutput/pgoutput.c

17. RelationSyncEntry
+
+ /* Indicate if no column is included in the publication */
+ bool no_cols_published;

Maybe this can have a more explanatory comment to explain why it is needed?

Fixed

~~~

18. check_and_init_gencol

+ bool found = false;
+ bool except_columns = false;
+
+ found = check_and_fetch_column_list(pub, entry->publish_as_relid, NULL,
+ NULL, &except_columns);
+
/*
* The column list takes precedence over the
* 'publish_generated_columns' parameter. Those will be checked later,
- * see pgoutput_column_list_init.
+ * see pgoutput_column_list_init. But when a column list is specified
+ * with EXCEPT, it should be checked.
*/
- if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+ if (found && !except_columns)
continue;

The variable 'found' seems a poor name; how about 'has_column_list' or similar?

Fixed

~~~

19. pgoutput_change

+ /*
+ * If all columns of a table is present in column list specified with
+ * EXCEPT, skip publishing the changes.
+ */
+ if (relentry->no_cols_published)
+ return;

/is present/are present/

fixed

======
src/bin/pg_dump/pg_dump.c

20. getPublicationTables

+ if (strcmp(prexcept, "t") == 0 && PQgetisnull(res, i, i_prattrs))
pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+ else
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
pubrinfo[j].dobj.catId.tableoid =
atooid(PQgetvalue(res, i, i_tableoid));
@@ -4797,6 +4797,7 @@ getPublicationTables(Archive *fout, TableInfo
tblinfo[], int numTables)
pubrinfo[j].pubrelqual = NULL;
else
pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+ pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);

Why not assign pubrinfo[j].pubexcept earlier so you don't have to
repeat the strcmp?

Fixed

~~~

21.
- if (strcmp(prexcept, "t") == 0)
+ if (strcmp(prexcept, "t") == 0 && PQgetisnull(res, i, i_prattrs))
simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);

Why not assign pubrinfo[j].pubexcept earlier so you don't have to
repeat the strcmp? Same also for the PQgetisnull(res, i,
i_prattrs))...

Fixed

~~~

22. dumpPublicationTable

if (pubrinfo->pubrattrs)
- appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+ {
+ if (pubrinfo->pubexcept)
+ appendPQExpBuffer(query, " EXCEPT (%s)", pubrinfo->pubrattrs);
+ else
+ appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+ }

SUGGESTION
{
if (pubrinfo->pubexcept)
appendPQExpBuffer(query, " EXCEPT");

appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
}

Fixed

I have addressed the comments shared by you and shared the updated v15
patch set here.

Thanks and Regards,
Shlok Kyal

Attachments:

v15-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v15-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 4520aa1878f27dfb3ca49da2a28436a074b31407 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v15 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 109 ++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 316 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..67211331efe 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,92 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+	ObjectAddress obj;
+	ListCell   *lc;
+	Oid			prid;
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, true);
+
+	/* Drop the relations associated with the publication */
+	rels = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	foreach(lc, rels)
+	{
+		Oid			relid = lfirst_oid(lc);
+
+		prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid,
+							   ObjectIdGetDatum(relid),
+							   ObjectIdGetDatum(pubid));
+
+		if (!OidIsValid(prid))
+			continue;
+
+		ObjectAddressSet(obj, PublicationRelRelationId, prid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1596,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 50f53159d58..e16f4832963 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10760,6 +10760,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10806,6 +10808,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 908eef97c6e..94ed3e8a776 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2242,7 +2242,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ba12678d1cb..905b58e0279 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4272,6 +4272,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index f1025fc0f19..788368d15ca 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1929,6 +1929,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index c9e309190df..84aea7027a1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1225,6 +1225,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v15-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v15-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 7c05c39516c0c971308dccdfa4892460d24794cf Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 27 Jun 2025 14:13:47 +0530
Subject: [PATCH v15 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  19 +-
 doc/src/sgml/ref/alter_publication.sgml.orig  | 275 ++++++++++++++++++
 doc/src/sgml/ref/create_publication.sgml      |  29 +-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  68 +++--
 src/backend/commands/publicationcmds.c        | 197 ++++++++++---
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  16 +-
 src/bin/pg_dump/pg_dump.c                     |  56 +++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++-
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++-
 src/test/regress/sql/publication.sql          |  46 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++
 26 files changed, 961 insertions(+), 128 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_publication.sgml.orig
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fa86c569dc4..4e37c928b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c32e6bc000d..3d0d29cf8b1 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2251,10 +2251,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..62273ed20dd 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -237,6 +244,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/alter_publication.sgml.orig b/doc/src/sgml/ref/alter_publication.sgml.orig
new file mode 100644
index 00000000000..178f39d9575
--- /dev/null
+++ b/doc/src/sgml/ref/alter_publication.sgml.orig
@@ -0,0 +1,275 @@
+<!--
+doc/src/sgml/ref/alter_publication.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterpublication">
+ <indexterm zone="sql-alterpublication">
+  <primary>ALTER PUBLICATION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER PUBLICATION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER PUBLICATION</refname>
+  <refpurpose>change the definition of a publication</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
+
+<phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
+
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The command <command>ALTER PUBLICATION</command> can change the attributes
+   of a publication.
+  </para>
+
+  <para>
+   The first three variants change which tables/schemas are part of the
+   publication.  The <literal>SET</literal> clause will replace the list of
+   tables/schemas in the publication with the specified list; the existing
+   tables/schemas that were present in the publication will be removed.  The
+   <literal>ADD</literal> and <literal>DROP</literal> clauses will add and
+   remove one or more tables/schemas from the publication.  Note that adding
+   tables/schemas to a publication that is already subscribed to will require an
+   <link linkend="sql-altersubscription-params-refresh-publication">
+   <literal>ALTER SUBSCRIPTION ... REFRESH PUBLICATION</literal></link> action on the
+   subscribing side in order to become effective. Note also that
+   <literal>DROP TABLES IN SCHEMA</literal> will not drop any schema tables
+   that were specified using
+   <link linkend="sql-createpublication-params-for-table"><literal>FOR TABLE</literal></link>/
+   <literal>ADD TABLE</literal>, and the combination of <literal>DROP</literal>
+   with a <literal>WHERE</literal> clause is not allowed.
+  </para>
+
+  <para>
+   The fourth variant of this command listed in the synopsis can change
+   all of the publication properties specified in
+   <xref linkend="sql-createpublication"/>.  Properties not mentioned in the
+   command retain their previous settings.
+  </para>
+
+  <para>
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
+  </para>
+
+  <para>
+   You must own the publication to use <command>ALTER PUBLICATION</command>.
+   Adding a table to a publication additionally requires owning that table.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
+   Also, the new owner of a
+   <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
+   or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
+   publication must be a superuser. However, a superuser can
+   change the ownership of a publication regardless of these restrictions.
+  </para>
+
+  <para>
+   Adding/Setting any schema when the publication also publishes a table with a
+   column list, and vice versa is not supported.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name of an existing publication whose definition is to be altered.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">table_name</replaceable></term>
+    <listitem>
+     <para>
+      Name of an existing table.  If <literal>ONLY</literal> is specified before the
+      table name, only that table is affected.  If <literal>ONLY</literal> is not
+      specified, the table and all its descendant tables (if any) are
+      affected.  Optionally, <literal>*</literal> can be specified after the table
+      name to explicitly indicate that descendant tables are included.
+     </para>
+
+     <para>
+      Optionally, a column list can be specified.  See <xref
+      linkend="sql-createpublication"/> for details. Note that a subscription
+      having several publications in which the same table has been published
+      with different column lists is not supported. See
+      <xref linkend="logical-replication-col-list-combining"/> for details of
+      potential problems when altering column lists.
+     </para>
+
+     <para>
+      If the optional <literal>WHERE</literal> clause is specified, rows for
+      which the <replaceable class="parameter">expression</replaceable>
+      evaluates to false or null will not be published. Note that parentheses
+      are required around the expression. The
+      <replaceable class="parameter">expression</replaceable> is evaluated with
+      the role used for the replication connection.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">schema_name</replaceable></term>
+    <listitem>
+     <para>
+      Name of an existing schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
+    <listitem>
+     <para>
+      This clause alters publication parameters originally set by
+      <xref linkend="sql-createpublication"/>.  See there for more information.
+     </para>
+     <caution>
+      <para>
+       Altering the <literal>publish_via_partition_root</literal> parameter can
+       lead to data loss or duplication at the subscriber because it changes
+       the identity and schema of the published tables. Note this happens only
+       when a partition root table is specified as the replication target.
+      </para>
+      <para>
+       This problem can be avoided by refraining from modifying partition leaf
+       tables after the <command>ALTER PUBLICATION ... SET</command> until the
+       <link linkend="sql-altersubscription"><command>ALTER SUBSCRIPTION ... REFRESH PUBLICATION</command></link>
+       is executed and by only refreshing using the <literal>copy_data = off</literal>
+       option.
+      </para>
+     </caution>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The user name of the new owner of the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name for the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Change the publication to publish only deletes and updates:
+<programlisting>
+ALTER PUBLICATION noinsert SET (publish = 'update, delete');
+</programlisting>
+  </para>
+
+  <para>
+   Add some tables to the publication:
+<programlisting>
+ALTER PUBLICATION mypublication ADD TABLE users (user_id, firstname), departments;
+</programlisting></para>
+
+  <para>
+   Change the set of columns published for a table:
+<programlisting>
+ALTER PUBLICATION mypublication SET TABLE users (user_id, firstname, lastname), TABLE departments;
+</programlisting></para>
+
+  <para>
+   Add schemas <structname>marketing</structname> and
+   <structname>sales</structname> to the publication
+   <structname>sales_publication</structname>:
+<programlisting>
+ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
+</programlisting>
+  </para>
+
+  <para>
+   Add tables <structname>users</structname>,
+   <structname>departments</structname> and schema
+   <structname>production</structname> to the publication
+   <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>ALTER PUBLICATION</command> is a <productname>PostgreSQL</productname>
+   extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createpublication"/></member>
+   <member><xref linkend="sql-droppublication"/></member>
+   <member><xref linkend="sql-createsubscription"/></member>
+   <member><xref linkend="sql-altersubscription"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..7fd8872db5f 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +458,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 95f4cac2467..73965ccd04c 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..ec580e3b050 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -861,13 +877,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +903,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +925,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1181,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 67211331efe..82500cf9fef 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1246,6 +1251,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1282,6 +1308,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	}
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1594,6 +1693,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1806,6 +1919,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1878,6 +1992,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1953,8 +2068,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e2b94c8c609..5f147f97aaf 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8624,7 +8624,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
@@ -18797,7 +18797,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e16f4832963..d7fe95a840f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -445,7 +445,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10612,7 +10613,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10632,12 +10633,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10675,6 +10677,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10750,6 +10753,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10762,6 +10784,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10788,6 +10812,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 693a766e6d7..5512b4cba7f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2063,7 +2063,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2174,22 +2175,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2208,7 +2193,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2223,6 +2209,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2301,6 +2289,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..bffdab2ab63 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5845,14 +5847,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5890,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5908,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1937997ea67..56c78b7441f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -183,6 +183,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4512,8 +4514,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4679,6 +4707,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4690,8 +4719,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4702,6 +4740,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4717,6 +4756,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4728,6 +4768,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4741,7 +4782,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4782,6 +4827,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11562,6 +11610,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19851,6 +19902,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 39eef1d6617..a9cbed8c9ce 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 0b0977788f1..56d6740b9ea 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e1cfa99874e..eabc78ee09f 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3303,6 +3303,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..10b5f7f29cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6712,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6736,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 94ed3e8a776..0f3c73da02f 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2245,11 +2245,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3536,7 +3541,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..33b771990bd 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetRelationPublications(),
@@ -163,7 +164,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +175,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 905b58e0279..d901cb0ffa7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4235,6 +4235,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4243,6 +4244,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 788368d15ca..0bf4582dd17 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -209,13 +209,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -234,8 +258,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1932,9 +1973,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1951,7 +1998,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1970,6 +2034,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1988,6 +2057,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2005,6 +2080,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2043,9 +2124,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 84aea7027a1..6e814edace6 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1228,17 +1241,31 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1246,6 +1273,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1253,6 +1283,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1260,6 +1294,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1276,10 +1314,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v15-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v15-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From fb8239afd58e466469b3e290e2daef329cc69f07 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 27 Jun 2025 12:01:02 +0530
Subject: [PATCH v15 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

The column "prexcept" of system catalog "pg_publication_rel" is set to
"true" when publication is created with EXCEPT table or EXCEPT column
list. If column "prattrs" of system catalog "pg_publication_rel" is also
set or column "puballtables" of system catalog "pg_publication" is
"false", it indicates the column list is specified with EXCEPT clause
and columns in "prattrs" are excluded from being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |  5 +-
 doc/src/sgml/logical-replication.sgml         | 89 ++++++++++++-----
 doc/src/sgml/ref/alter_publication.sgml       | 10 +-
 doc/src/sgml/ref/create_publication.sgml      | 40 +++++---
 src/backend/catalog/pg_publication.c          | 64 ++++++++++--
 src/backend/commands/publicationcmds.c        | 31 ++++--
 src/backend/parser/gram.y                     | 65 ++++++++++++
 src/backend/replication/pgoutput/pgoutput.c   | 61 ++++++++++--
 src/bin/pg_dump/pg_dump.c                     | 45 +++++----
 src/bin/pg_dump/pg_dump.h                     |  1 +
 src/bin/psql/describe.c                       | 85 +++++++++++-----
 src/include/catalog/pg_publication.h          |  6 +-
 src/include/catalog/pg_publication_rel.h      |  5 +-
 src/test/regress/expected/publication.out     | 61 ++++++++++++
 src/test/regress/sql/publication.sql          | 42 ++++++++
 .../t/036_rep_changes_except_table.pl         | 98 ++++++++++++++++++-
 16 files changed, 603 insertions(+), 105 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4e37c928b44..fef1e803b60 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is NULL,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 3d0d29cf8b1..0ddc658cf7c 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1340,10 +1340,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1358,7 +1358,9 @@ Publications:
   <para>
    If no column list is specified, any columns added to the table later are
    automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   all columns is not the same as having no column list at all. Similarly, if an
+   column list is specified with EXCEPT, any columns added to the table later
+   are also replicated automatically.
   </para>
 
   <para>
@@ -1391,11 +1393,13 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with EXCEPT clause
+   must not include the table's replica identity columns (see
    <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with EXCEPT clause may include replica identity columns.
   </para>
 
   <para>
@@ -1440,18 +1444,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the EXCEPT clause to reduce the number of
+    columns that will be replicated. Note that the order of column names in
+    the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1459,12 +1466,13 @@ Publications:
      for each publication.
 <programlisting>
 /* pub # */ \dRp+
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+ Owner  | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
+--------+------------+---------+---------+---------+-----------+-------------------+----------
+ ubuntu | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1485,23 +1493,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1513,11 +1539,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1526,6 +1562,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 62273ed20dd..6967a4aadc7 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -259,6 +259,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 7fd8872db5f..e056e405829 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without EXCEPT is specified, only the named columns are
+      replicated. The column list can contain stored generated columns as well.
+      If the column list is omitted, the publication will replicate
+      all non-generated columns (including any added in the future) by default.
+      Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -328,9 +335,11 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
   <para>
    Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   and any column list specified with EXCEPT must not include the
+   <literal>REPLICA IDENTITY</literal> columns in order for
+   <command>UPDATE</command> or <command>DELETE</command> operations to be
+   published. There are no column list restrictions if the publication publishes
+   only <command>INSERT</command> operations.
   </para>
 
   <para>
@@ -474,6 +483,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ec580e3b050..fb817661326 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,10 +263,13 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
@@ -296,6 +299,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -646,10 +659,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the exceptcols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *exceptcols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -672,6 +687,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (exceptcols && bms_is_member(att->attnum, exceptcols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -776,8 +794,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -1263,6 +1283,9 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Datum		exceptDatum;
+		bool		isnull;
+		bool		except_columns = false;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1287,7 +1310,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
+			exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+										  Anum_pg_publication_rel_prexcept,
+										  &isnull);
+
+			/*
+			 * We fetch pubtuple if publication is not FOR ALL TABLES and not
+			 * FOR TABLES IN SCHEMA. So if prexcept is true, it indicate that
+			 * prattrs contains columns to be excluded for replication.
+			 */
+			if (!isnull)
+				except_columns = DatumGetBool(exceptDatum);
+
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
@@ -1303,15 +1337,24 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
 			int16	   *attnums;
 			TupleDesc	desc = RelationGetDescr(rel);
+			Bitmapset  *columns = NULL;
 			int			i;
 
+			/* If a column list is specified with EXCEPT */
+			if (except_columns && !nulls[2])
+				columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+
 			attnums = (int16 *) palloc(desc->natts * sizeof(int16));
 
 			for (i = 0; i < desc->natts; i++)
@@ -1335,6 +1378,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 82500cf9fef..2db32e1d1a9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,7 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ * 	  If any column is missing, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -381,6 +381,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +405,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +496,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1441,6 +1449,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1456,6 +1465,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		exceptDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1472,6 +1482,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull)
+					oldexcept = DatumGetBool(exceptDatum);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1503,7 +1520,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
@@ -1520,6 +1538,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7fe95a840f..b0045a56c6c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,6 +446,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list except_pub_obj_list
+				opt_except_column_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -4413,6 +4414,10 @@ opt_column_list:
 			| /*EMPTY*/								{ $$ = NIL; }
 		;
 
+opt_except_column_list:
+			'(' columnList ')'						{ $$ = $2; }
+		;
+
 columnList:
 			columnElem								{ $$ = list_make1($1); }
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
@@ -10679,6 +10684,17 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| TABLE relation_expr EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $2;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->pubtable->except = true;
+					$$->location = @1;
+				}
 			| TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10719,6 +10735,34 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
+			| ColId EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					/*
+					 * If either a row filter or exclude column list is
+					 * specified, create a PublicationTable object.
+					 */
+					if ($3 || $4)
+					{
+						/*
+						 * The OptWhereClause must be stored here but it is
+						 * valid only for tables. For non-table objects, an
+						 * error will be thrown later via
+						 * preprocess_pubobj_list().
+						 */
+						$$->pubtable = makeNode(PublicationTable);
+						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
+						$$->pubtable->except = true;
+					}
+					else
+					{
+						$$->name = $1;
+					}
+					$$->location = @1;
+				}
 			| ColId indirection opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10729,6 +10773,17 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| ColId indirection EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->pubtable->except = true;
+					$$->location = @1;
+				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
 			| extended_relation_expr opt_column_list OptWhereClause
 				{
@@ -10739,6 +10794,16 @@ PublicationObjSpec:
 					$$->pubtable->columns = $2;
 					$$->pubtable->whereClause = $3;
 				}
+			| extended_relation_expr EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $1;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = true;
+				}
 			| CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 5512b4cba7f..7bceb09c2ec 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT clause in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'no_cols_published' flag is introduced.
+	 */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1066,12 +1076,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1120,11 +1139,30 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		no_col_published = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				no_col_published = true;
+		}
 
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
@@ -1132,7 +1170,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		 * of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!no_col_published && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1144,7 +1182,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1154,9 +1192,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->no_cols_published = no_col_published;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published != no_col_published) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1480,6 +1520,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2057,6 +2104,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2106,6 +2154,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 56c78b7441f..4ac9d84bb3b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4781,24 +4781,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4824,10 +4807,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4907,7 +4909,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index a9cbed8c9ce..3b3d867db58 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -682,6 +682,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 10b5f7f29cb..a55c4c6505d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,14 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,37 +3040,61 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
-
-				appendPQExpBuffer(&buf,
+								  "WHERE pr.prrelid = '%s' "
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3106,8 +3132,15 @@ describeOneTableDetails(const char *schemaname,
 
 				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
+				{
+					if (!PQgetisnull(result, i, 3) &&
+						strcmp(PQgetvalue(result, i, 3), "t") == 0)
+						appendPQExpBuffer(&buf, " EXCEPT (%s)",
+										  PQgetvalue(result, i, 2));
+					else
+						appendPQExpBuffer(&buf, " (%s)",
+										  PQgetvalue(result, i, 2));
+				}
 
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 33b771990bd..498320c7fae 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -180,7 +180,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -190,6 +191,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *exceptcols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 0bf4582dd17..20f29cec77b 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2131,6 +2131,67 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  syntax error at or near ";"
+LINE 1: ...BLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+                                                                      ^
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {c,d}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b}     | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,b,c,d} | 
+(2 rows)
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6e814edace6..b897918bcc1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1322,6 +1322,48 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
index 1d115283809..6535945d064 100644
--- a/src/test/subscription/t/036_rep_changes_except_table.pl
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -1,7 +1,7 @@
 
 # Copyright (c) 2021-2022, PostgreSQL Global Development Group
 
-# Logical replication tests for except table publications
+# Logical replication tests for except table and except column publications
 use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
@@ -77,6 +77,102 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*), min(a), max(a) FROM public.tab1");
 is($result, qq(0||), 'check rows on subscriber catchup');
 
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2 EXCEPT (b, c)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+	'check that initial sync for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is($result, qq(1||), 'check that initial sync for except column publication');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+# Test incremental changes
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is( $result, qq(1||
+4||), 'check incremental insert for except column publication');
+
+# Test for update
+$node_publisher->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_subscriber->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET a = 3, b = 4, c = 5 WHERE a = 1");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|5|6
+|4|5),
+	'check update for except column publication');
+
+# Test ALTER PUBLICATION for EXCEPT (col_list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab3 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3");
+is($result, qq(1||3), 'check alter publication with EXCEPT');
+
+# Test for publication created on table with generated columns and column list
+# specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET (publish_generated_columns)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4");
+is( $result, qq(1||3
+2||6), 'check publication with generated columns and EXCEPT');
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
-- 
2.34.1

#88Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#86)
Re: Skipping schema changes in publication

On Thu, 26 Jun 2025 at 15:27, shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Jun 24, 2025 at 9:48 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have included the changes for
it in v14-0003 patch.

Thanks for the patches. I have reviewed patch001 alone, please find
few comments:

1)
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
</para>

It is misleading, as far as I have understood, we do not drop the
tables or schemas associated with the pub; we just remove those from
the publication's object list. See previous doc:
"The ADD and DROP clauses will add and remove one or more
tables/schemas from the publication"

Perhaps we want to say the same thing when we speak about the 'drop'
aspect of RESET.

I have updated the document.

2)
AlterPublicationReset():

+ if (!OidIsValid(prid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("relation \"%s\" is not part of the publication",
+ get_rel_name(relid))));

Can you please help me understand which scenario will give this error?

Another question is do we really need this error? IIUC, we generally
give errors if a user has explicitly called out a name of an object
and that object is not found. Example:

postgres=# alter publication pubnew drop table t1,tab2;
ERROR: relation "t1" is not part of the publication

While in a few other cases, we pass missing_okay as true and do not
give errors. Please see other callers of performDeletion in
publicationcmds.c itself. There we have usage of missing_okay=true. I
have not researched myself, but please analyze the cases where
missing_okay is passed as true to figure out if those match our RESET
case. Try to reproduce if possible and then take a call.

I thought about the above point and I also think this check is not
required. Also, the function was calling PublicationDropSchemas with
missing_ok as false. I have changed it to be true.

3)
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...

There is a problem in syntax, I think the intention of testcase was to
run this query successfully.

I have fixed it.

Thanks Shveta for reviewing the patch. I have addressed the comments
and posted an updated version v15 in [1]/messages/by-id/CANhcyEU+aPu6iAH2cTA0cDtn3pd6c_njBONCt3FubYZoEEnm8Q@mail.gmail.com.

[1]: /messages/by-id/CANhcyEU+aPu6iAH2cTA0cDtn3pd6c_njBONCt3FubYZoEEnm8Q@mail.gmail.com

Thanks and Regards,
Shlok Kyal

#89Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#87)
Re: Skipping schema changes in publication

Hi Shlok.

Some review comments for v15-0003.

======
doc/src/sgml/catalogs.sgml

1.
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is NULL,
+       then exclude the entire relation.
       </para></entry>

I noticed other fields on this page say "null" instead of "NULL". It
seems like "null" is more conventional.

======
doc/src/sgml/logical-replication.sgml

2.
   <para>
    If no column list is specified, any columns added to the table later are
    automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   all columns is not the same as having no column list at all.
Similarly, if an
+   column list is specified with EXCEPT, any columns added to the table later
+   are also replicated automatically.
   </para>

2a.
CURRENTLY
If no column list or a column list with EXCEPT is specified, any
columns added to the table later are automatically replicated. This
means that having a column list which names all columns is not the
same as having no column list at all. If an column list is specified,
any columns added to the table later are automatically replicated.

~

That still doesn't quite make sense. I think instead of saying "This
means..." it needs to say something a bit like below:

However, a normal column list (without EXCEPT) only replicates the
specified columns and no more. Therefore, having a column list that
names all columns is not the same as having no column list at all, as
more columns may be added to the table later.

~

2b.
And the final sentence "If an column list..." looks like a cut/paste error (??)

~

2c.
Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

~~~

2.5A.
The description about generated columns still says this:

CURRENT:
Generated columns can also be specified in a column list. This allows
generated columns to be published, regardless of the publication
parameter publish_generated_columns. See Section 29.6 for details.

~

But I don't think it is quite correct. IMO gencols behaviour is much
more subtle...

e.g.

a) Normal collist - these named cols are published REGARDLESS of the
'publish_generated_cols' parameter (same as before)

b) EXCEPT collist - you can specify gencols in the list REGARDLESS of
the 'publish_generated_cols' parameter, because since they are named
as "except" then they will not be published anyhow....

c) BUT for EXCEPT collist case, I think any gencols that are *not*
covered by that EXCEPT collist should follow the rules according to
the 'publish_generated_cols' parameter.

So, it is much more tricky than the docs currently say:

Also

2.5B.
- The text says "See Section 29.6 for details," but there are no
examples of these combinations (e.g. EXCEPT collist and diff parameter
setting)

2.5C,
- The regression tests also need to be more complex to cover these

2.5D.
- You might need to add something in the CREATE PUBLICATION "NOTES"
section after all -- even if it just refers to here.

~~~

3.
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the EXCEPT clause to reduce the number of
+    columns that will be replicated. Note that the order of column names in
+    the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>

Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

======
doc/src/sgml/ref/create_publication.sgml

4.
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if
<literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without EXCEPT is specified, only the named
columns are
+      replicated. The column list can contain stored generated columns as well.
+      If the column list is omitted, the publication will replicate
+      all non-generated columns (including any added in the future) by default.
+      Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>

Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

~~~

5.
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>

Maybe here EXCEPT should be written as <literal>EXCEPT</literal>.

** Note all the extra subtleties that I mentioned in the review
comment #2.5 above --- e.g. IMO any *un-listed* gencols still should
follow the parameter rules.

~~~

6.
   <para>
    Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   and any column list specified with EXCEPT must not include the
+   <literal>REPLICA IDENTITY</literal> columns in order for
+   <command>UPDATE</command> or <command>DELETE</command> operations to be
+   published. There are no column list restrictions if the
publication publishes
+   only <command>INSERT</command> operations.
   </para>

6a.
CURRENT:
Any column list must include the REPLICA IDENTITY columns, and any
column list specified with EXCEPT must not include the REPLICA
IDENTITY columns in order for UPDATE or DELETE operations to be
published.

~

I felt that might be better expressed the other way around. Also, it
might be better to say "not name" instead of "not include" because
EXCEPT + include seemed a bit contrary.

SUGGESTION (maybe like this)
In order for UPDATE or DELETE operations to work, all the REPLICA
IDENTITY columns must be published. So, any column list must name all
REPLICA IDENTITY columns, and any EXCEPT column list must not name any
REPLICA IDENTITY columns.

~~

6b.
Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

======
src/backend/catalog/pg_publication.c

check_and_fetch_column_list:

7.
+ /* Lookup the except attribute */
+ cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+   Anum_pg_publication_rel_prexcept, &isnull);
+
+ if (!isnull)
+ {
+ Assert(!pub->alltables);
+ *except_columns = DatumGetBool(cfdatum);
+ }
+

I felt it would be safer to also assign *except_columns = false;
up-front so the caller could be sure this flag was meaningful on
return.

~~~

pub_form_cols_map:

8.
Maybe use snake case like for other params, so /excepcols/except_cols/

~~~

pg_get_publication_tables:

9.

I felt all the logic in this function maybe can be simpler:

e.g. If you just have "Bitmapset *except_columns = NULL;" then null
nmeans there is no except columns; otherwise there is. This means you
don't need a separate 'bool except_column' variable.

e.g. Assign the Bitmapset *except_columns after you already have the
values[2], instead of doing it later.

e.g. The skip code if (except_columns && bms_is_member(att->attnum,
columns)) could just check the list member, I think, without the
additional bool.

~~~

10.
+ /*
+ * We fetch pubtuple if publication is not FOR ALL TABLES and not
+ * FOR TABLES IN SCHEMA. So if prexcept is true, it indicate that
+ * prattrs contains columns to be excluded for replication.
+ */
+ if (!isnull)
+ except_columns = DatumGetBool(exceptDatum);

/indicate/indicates/

======
src/backend/parser/gram.y

11.
+ | TABLE relation_expr EXCEPT opt_except_column_list OptWhereClause
+ {
+ $$ = makeNode(PublicationObjSpec);
+ $$->pubobjtype = PUBLICATIONOBJ_TABLE;
+ $$->pubtable = makeNode(PublicationTable);
+ $$->pubtable->relation = $2;
+ $$->pubtable->columns = $4;
+ $$->pubtable->whereClause = $5;
+ $$->pubtable->except = true;
+ $$->location = @1;
+ }

I wasn't expecting you would need another 'opt_except_column_list' and
all the code duplication that causes. AFAIK, the syntax is identical
for 'opt_column_list' apart from the preceding EXCEPT so I thought all
you need is to allow the 'opt_column_list' to have an optional EXCEPT
qualifier.

======
src/backend/replication/pgoutput/pgoutput.c

12.
+
+ /*
+ * Indicates whether no columns are published for a given relation. With
+ * the introduction of the EXCEPT clause in column lists, it is now
+ * possible to define a publication that excludes all columns of a table.
+ * However, the 'columns' attribute cannot represent this case, since a
+ * NULL value implies that all columns are published. To distinguish this
+ * scenario, the 'no_cols_published' flag is introduced.
+ */
+ bool no_cols_published;
 } RelationSyncEntry;

But, what about when Bitmapset *columns is not null, but has no bits
set -- doesn't that mean the same as "no columns"?

======
src/include/catalog/pg_publication.h

13.
 extern Bitmapset *pub_form_cols_map(Relation relation,
- PublishGencolsType include_gencols_type);
+ PublishGencolsType include_gencols_type,
+ Bitmapset *exceptcols);

Maybe snake-case like the other params: /exceptcols/except_cols/

======
src/test/regress/sql/publication.sql

14.
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1,
pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+

I think tests should also use psql \dRp+ commands in places to show
that the "describe" stuff is working correctly.

~~~

15.
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1,
TABLE pub_test_except1 EXCEPT (b, c);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;

Should explain more about what you are testing here:
a) cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
b) syntax error EXCEPT without a col-list

~~~

16.
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT
(a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';

The comment is a bit misleading because there are many kinds of
"alter". Maybe say more like
Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)

~~~

17.
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
Should explain more:
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+-- Verify ok - ALTER PUBLICATION ... DROP ...

~~~

18.
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';

Missing comment:
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)

~~~

19.
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+DROP INDEX pub_test_except1_a_idx;

19a.
IIUC, really there are multiple tests here, so I think it should all
be split and commented separately.

a) Verify that EXCEPT col-list cannot contain RI cols (when using RI FULL)
b) Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
c) Verify that so long as no clash between RI cols and the EXCEPT
col-list, then it is ok

~

19b.
IMO, some index names could be better:

CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
How about 'pub_test_except1_ac_idx'?

~~~

20.
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;

Add a "cleanup" comment.

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

#90Peter Smith
smithpb2250@gmail.com
In reply to: Peter Smith (#89)
Re: Skipping schema changes in publication

Hi Shlok,

One more thing, I noticed there is no tab-completion code yet for this
new EXCEPT (column_list) syntax.

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

#91shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#88)
Re: Skipping schema changes in publication

On Fri, Jun 27, 2025 at 3:44 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 26 Jun 2025 at 15:27, shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Jun 24, 2025 at 9:48 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have included the changes for
it in v14-0003 patch.

Thanks for the patches. I have reviewed patch001 alone, please find
few comments:

1)
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
</para>

It is misleading, as far as I have understood, we do not drop the
tables or schemas associated with the pub; we just remove those from
the publication's object list. See previous doc:
"The ADD and DROP clauses will add and remove one or more
tables/schemas from the publication"

Perhaps we want to say the same thing when we speak about the 'drop'
aspect of RESET.

I have updated the document.

2)
AlterPublicationReset():

+ if (!OidIsValid(prid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("relation \"%s\" is not part of the publication",
+ get_rel_name(relid))));

Can you please help me understand which scenario will give this error?

Another question is do we really need this error? IIUC, we generally
give errors if a user has explicitly called out a name of an object
and that object is not found. Example:

postgres=# alter publication pubnew drop table t1,tab2;
ERROR: relation "t1" is not part of the publication

While in a few other cases, we pass missing_okay as true and do not
give errors. Please see other callers of performDeletion in
publicationcmds.c itself. There we have usage of missing_okay=true. I
have not researched myself, but please analyze the cases where
missing_okay is passed as true to figure out if those match our RESET
case. Try to reproduce if possible and then take a call.

I thought about the above point and I also think this check is not
required. Also, the function was calling PublicationDropSchemas with
missing_ok as false. I have changed it to be true.

Okay. Is there a reason for not using PublicationDropTables() here? We
have rewritten similar code in the Reset flow.

3)
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...

There is a problem in syntax, I think the intention of testcase was to
run this query successfully.

I have fixed it.

Thanks Shveta for reviewing the patch. I have addressed the comments
and posted an updated version v15 in [1].

Thanks for the patches. My review is in progress but please find few
comments on 002:

1)
where exception_object is:
[ ONLY ] table_name [ * ]

We have the above in CREATE and ALTER pub docs, but we do not explain
ONLY with EXCEPT. We do have an explanation of ONLY under 'FOR TABLE'.
But since 'FOR TABLE' and 'EXCEPT' do not go together, it is somewhat
difficult to connect the dots and find the information ONLY in the
context of EXCEPT. We shall have ONLY explained for EXCEPT as well. Or
we can have ONLY defined in a way that both 'FOR TABLE' and 'EXCEPT'
can refer to it.

2)
We get tab-completion options in this command:
postgres=# create publication pub5 for TABLE tab1 W
WHERE ( WITH (

Similarly in this command:
create publication pub5 for TABLES IN SCHEMA s1

But once we have 'EXCEPT TABLE', we do not get further tab-completion
option like WITH(...)
create publication pub5 for ALL TABLES EXCEPT TABLE tab1

3)
During tab-expansion, 'EXCEPT TABLE' and 'WITH (' in the below
command looks like they are connecting words. Can the gap be increased
similar to tab-expansion of next command shown below:

postgres=# create publication pub4 for ALL TABLES
EXCEPT TABLE WITH (

postgres=# create publication pub4 for
ALL TABLES TABLE TABLES IN SCHEMA

4)
alter_publication.sgml.orig is a left-over in patch002.

thanks
Shveta

#92shveta malik
shveta.malik@gmail.com
In reply to: shveta malik (#91)
Re: Skipping schema changes in publication

Few more comments on 002:

5)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
{

+ List    *exceptlist;
+
+ exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);

a) Here, we are assuming that the list provided by
GetPublicationRelations() will be except-tables list only, but there
is no validation of that.
b) We are using GetPublicationRelations() to get the relations which
are excluded from the publication. The name of function and comments
atop function are not in alignment with this usage.

Suggestion:
We can have a new GetPublicationExcludeRelations() function for the
concerned usage. The existing logic of GetPublicationRelations() can
be shifted to a new internal-logic function which will accept a
'except-flag' as well. Both GetPublicationRelations() and
GetPublicationExcludeRelations() can call that new function by passing
'except-flag' as false and true respectively. The new internal
function will validate 'prexcept' against that except-flag passed and
will return the results.

6)
Before your patch002, GetTopMostAncestorInPublication() was checking
pg_publication_rel and pg_publication_namespace to find out if the
table in the ancestor-list is part of a given particular. Both
pg_publication_rel and pg_publication_namespace did not have the entry
"for all tables" publications. That means
GetTopMostAncestorInPublication() was originally not checking whether
the given puboid is an "for all tables" publication to see if a rel
belongs to that particular pub or not. I

But now with the current change, we do check if pub is all-tables pub,
if so, return relid and mark ancestor_level (provided table is not
part of the except list). IIUC, the result in 2 cases may be
different. Is that the intention? Let me know if my understanding is
wrong.

thanks
Shveta

#93Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#89)
3 attachment(s)
Re: Skipping schema changes in publication

On Mon, 30 Jun 2025 at 11:37, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Some review comments for v15-0003.

======
doc/src/sgml/catalogs.sgml

1.
<para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is NULL,
+       then exclude the entire relation.
</para></entry>

I noticed other fields on this page say "null" instead of "NULL". It
seems like "null" is more conventional.

Fixed

======
doc/src/sgml/logical-replication.sgml

2.
<para>
If no column list is specified, any columns added to the table later are
automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   all columns is not the same as having no column list at all.
Similarly, if an
+   column list is specified with EXCEPT, any columns added to the table later
+   are also replicated automatically.
</para>

2a.
CURRENTLY
If no column list or a column list with EXCEPT is specified, any
columns added to the table later are automatically replicated. This
means that having a column list which names all columns is not the
same as having no column list at all. If an column list is specified,
any columns added to the table later are automatically replicated.

~

That still doesn't quite make sense. I think instead of saying "This
means..." it needs to say something a bit like below:

However, a normal column list (without EXCEPT) only
specified columns and no more. Therefore, having a column list that
names all columns is not the same as having no column list at all, as
more columns may be added to the table later.

Fixed

~

2b.
And the final sentence "If an column list..." looks like a cut/paste error (??)

Yes it was a mistake.

~

2c.
Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

Fixed.

~~~

2.5A.
The description about generated columns still says this:

CURRENT:
Generated columns can also be specified in a column list. This allows
generated columns to be published, regardless of the publication
parameter publish_generated_columns. See Section 29.6 for details.

~

But I don't think it is quite correct. IMO gencols behaviour is much
more subtle...

e.g.

a) Normal collist - these named cols are published REGARDLESS of the
'publish_generated_cols' parameter (same as before)

b) EXCEPT collist - you can specify gencols in the list REGARDLESS of
the 'publish_generated_cols' parameter, because since they are named
as "except" then they will not be published anyhow....

c) BUT for EXCEPT collist case, I think any gencols that are *not*
covered by that EXCEPT collist should follow the rules according to
the 'publish_generated_cols' parameter.

So, it is much more tricky than the docs currently say:

Modified the documentation

Also

2.5B.
- The text says "See Section 29.6 for details," but there are no
examples of these combinations (e.g. EXCEPT collist and diff parameter
setting)

Added documentation.

2.5C,
- The regression tests also need to be more complex to cover these

Added tests related to these

2.5D.
- You might need to add something in the CREATE PUBLICATION "NOTES"
section after all -- even if it just refers to here.

Added documentation

~~~

3.
<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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the EXCEPT clause to reduce the number of
+    columns that will be replicated. Note that the order of column names in
+    the column lists does not matter.
<programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
</programlisting></para>

Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

Fixed

======
doc/src/sgml/ref/create_publication.sgml

4.
<para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if
<literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without EXCEPT is specified, only the named
columns are
+      replicated. The column list can contain stored generated columns as well.
+      If the column list is omitted, the publication will replicate
+      all non-generated columns (including any added in the future) by default.
+      Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
<xref linkend="logical-replication-col-lists"/> for details about column
lists.
</para>

Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

Fixed

~~~

5.
+     <para>
+      When a column list is specified with EXCEPT, the named columns are not
+      replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>

Maybe here EXCEPT should be written as <literal>EXCEPT</literal>.

Fixed

** Note all the extra subtleties that I mentioned in the review
comment #2.5 above --- e.g. IMO any *un-listed* gencols still should
follow the parameter rules.

~~~

6.
<para>
Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   and any column list specified with EXCEPT must not include the
+   <literal>REPLICA IDENTITY</literal> columns in order for
+   <command>UPDATE</command> or <command>DELETE</command> operations to be
+   published. There are no column list restrictions if the
publication publishes
+   only <command>INSERT</command> operations.
</para>

6a.
CURRENT:
Any column list must include the REPLICA IDENTITY columns, and any
column list specified with EXCEPT must not include the REPLICA
IDENTITY columns in order for UPDATE or DELETE operations to be
published.

~

I felt that might be better expressed the other way around. Also, it
might be better to say "not name" instead of "not include" because
EXCEPT + include seemed a bit contrary.

SUGGESTION (maybe like this)
In order for UPDATE or DELETE operations to work, all the REPLICA
IDENTITY columns must be published. So, any column list must name all
REPLICA IDENTITY columns, and any EXCEPT column list must not name any
REPLICA IDENTITY columns.

Fixed

~~

6b.
Maybe here EXCEPT should be written as <literal>EXCEPT</literal>

Fixed

======
src/backend/catalog/pg_publication.c

check_and_fetch_column_list:

7.
+ /* Lookup the except attribute */
+ cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+   Anum_pg_publication_rel_prexcept, &isnull);
+
+ if (!isnull)
+ {
+ Assert(!pub->alltables);
+ *except_columns = DatumGetBool(cfdatum);
+ }
+

I felt it would be safer to also assign *except_columns = false;
up-front so the caller could be sure this flag was meaningful on
return.

Fixed

~~~

pub_form_cols_map:

8.
Maybe use snake case like for other params, so /excepcols/except_cols/

Fixed

~~~

pg_get_publication_tables:

9.

I felt all the logic in this function maybe can be simpler:

e.g. If you just have "Bitmapset *except_columns = NULL;" then null
nmeans there is no except columns; otherwise there is. This means you
don't need a separate 'bool except_column' variable.

e.g. Assign the Bitmapset *except_columns after you already have the
values[2], instead of doing it later.

e.g. The skip code if (except_columns && bms_is_member(att->attnum,
columns)) could just check the list member, I think, without the
additional bool.

~~~

Fixed

10.
+ /*
+ * We fetch pubtuple if publication is not FOR ALL TABLES and not
+ * FOR TABLES IN SCHEMA. So if prexcept is true, it indicate that
+ * prattrs contains columns to be excluded for replication.
+ */
+ if (!isnull)
+ except_columns = DatumGetBool(exceptDatum);

/indicate/indicates/

Fixed

======
src/backend/parser/gram.y

11.
+ | TABLE relation_expr EXCEPT opt_except_column_list OptWhereClause
+ {
+ $$ = makeNode(PublicationObjSpec);
+ $$->pubobjtype = PUBLICATIONOBJ_TABLE;
+ $$->pubtable = makeNode(PublicationTable);
+ $$->pubtable->relation = $2;
+ $$->pubtable->columns = $4;
+ $$->pubtable->whereClause = $5;
+ $$->pubtable->except = true;
+ $$->location = @1;
+ }

I wasn't expecting you would need another 'opt_except_column_list' and
all the code duplication that causes. AFAIK, the syntax is identical
for 'opt_column_list' apart from the preceding EXCEPT so I thought all
you need is to allow the 'opt_column_list' to have an optional EXCEPT
qualifier.

The main reason I used a separate 'opt_except_column_list' is because
'opt_column_list' can also be NULL. But the column list specified with
EXCEPT not be NULL. So, 'opt_except_column_list' is defined such that
it cannot be null.

======
src/backend/replication/pgoutput/pgoutput.c

12.
+
+ /*
+ * Indicates whether no columns are published for a given relation. With
+ * the introduction of the EXCEPT clause in column lists, it is now
+ * possible to define a publication that excludes all columns of a table.
+ * However, the 'columns' attribute cannot represent this case, since a
+ * NULL value implies that all columns are published. To distinguish this
+ * scenario, the 'no_cols_published' flag is introduced.
+ */
+ bool no_cols_published;
} RelationSyncEntry;

But, what about when Bitmapset *columns is not null, but has no bits
set -- doesn't that mean the same as "no columns"?

I think this is possible. A bitmapset which has no set bit is NULL. I
saw following comment in bitmapset.c
"By convention, we always represent a set with
* the minimum possible number of words, i.e, there are never any trailing
* zero words. Enforcing this requires that an empty set is represented as
* NULL. Because an empty Bitmapset is represented as NULL, a non-NULL
* Bitmapset always has at least 1 Bitmapword."

======
src/include/catalog/pg_publication.h

13.
extern Bitmapset *pub_form_cols_map(Relation relation,
- PublishGencolsType include_gencols_type);
+ PublishGencolsType include_gencols_type,
+ Bitmapset *exceptcols);

Maybe snake-case like the other params: /exceptcols/except_cols/

Fixed

======
src/test/regress/sql/publication.sql

14.
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1,
pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+

I think tests should also use psql \dRp+ commands in places to show
that the "describe" stuff is working correctly.

~~~

Fixed

15.
+-- Check for invalid cases
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1,
TABLE pub_test_except1 EXCEPT (b, c);
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;

Should explain more about what you are testing here:
a) cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
b) syntax error EXCEPT without a col-list

~~~

fixed

16.
+-- Verify that publication can be altered with EXCEPT
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT
(a, b), pub_sch1.pub_test_except2;
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';

The comment is a bit misleading because there are many kinds of
"alter". Maybe say more like
Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)

~~~

Fixed

17.
+-- Verify ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
Should explain more:
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+-- Verify ok - ALTER PUBLICATION ... DROP ...

~~~

Fixed

18.
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';

Missing comment:
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)

~~~

Fixed

19.
+-- Verify excluded columns cannot be part of REPLICA IDENTITY
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_a_idx;
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+DROP INDEX pub_test_except1_a_idx;

19a.
IIUC, really there are multiple tests here, so I think it should all
be split and commented separately.

a) Verify that EXCEPT col-list cannot contain RI cols (when using RI FULL)
b) Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
c) Verify that so long as no clash between RI cols and the EXCEPT
col-list, then it is ok

~

Fixed

19b.
IMO, some index names could be better:

CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a, c);
How about 'pub_test_except1_ac_idx'?

~~~

Fixed

20.
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;

Add a "cleanup" comment.

Added

I have addressed the comments and added the latest v16.

Thanks and Regards,
Shlok Kyal

Attachments:

v16-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v16-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 2777628286147b443d8ab01003d7cf0ec9448b95 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 16 Jul 2025 10:56:14 +0530
Subject: [PATCH v16 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  21 +-
 doc/src/sgml/ref/create_publication.sgml      |  37 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          | 103 ++++++---
 src/backend/commands/publicationcmds.c        | 198 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |   8 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  46 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++++
 25 files changed, 731 insertions(+), 133 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0d23bc1b122..1bb1db26045 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index e26f7f59d4a..2e9f6019474 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2283,10 +2283,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..bd25a1a723c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected.
      </para>
 
      <para>
@@ -237,6 +244,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..a2f9c0d4825 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitions of
+      a partitioned table are always implicitly considered part of the
+      publication, so they are never explicitly excluded from the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +466,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 4f7b11175c6..cb4215071d0 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..1878fba8748 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +787,16 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication. If except_table is true, the
+ * list contains relations oids that excluded from publication, else the list
+ * contains the relation oids that are part of publication.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
-List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+static List *
+GetPubIncludedOrExcludedRels(Oid pubid, PublicationPartOpt pub_partopt,
+							 bool except_table)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +821,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if ((except_table && pubrel->prexcept) || !except_table)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -816,6 +838,25 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 	return result;
 }
 
+/*
+ * Gets list of relation oids for a publication.
+ *
+ * This should only be used FOR TABLE publications, the FOR ALL TABLES
+ * should use GetAllTablesPublicationRelations().
+ */
+List *
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	return GetPubIncludedOrExcludedRels(pubid, pub_partopt, false);
+}
+
+/* Get list of relation oids excluded from the publication */
+List *
+GetPublicationExcludeRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	return GetPubIncludedOrExcludedRels(pubid, pub_partopt, true);
+}
+
 /*
  * Gets list of publication oids for publications marked as FOR ALL TABLES.
  */
@@ -861,13 +902,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationExcludeRelations(pubid, pubviaroot ? PUBLICATION_PART_ALL : PUBLICATION_PART_ROOT);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +928,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +950,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1206,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 4d1ed875849..d25de331c34 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1281,6 +1307,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1420,6 +1519,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1593,6 +1693,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1805,6 +1919,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1877,6 +1992,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1952,8 +2068,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..47916ef32ae 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 850d0fd2fd5..bdbbcccd47f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -445,7 +445,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10677,7 +10678,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10697,12 +10698,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10740,6 +10742,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10815,6 +10818,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10827,6 +10849,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10853,6 +10877,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index f4c977262c5..08111b571de 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..5d55f1f4ece 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c6226175528..79747f9a99f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -183,6 +183,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4512,8 +4514,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4679,6 +4707,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4690,8 +4719,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4702,6 +4740,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4717,6 +4756,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4728,6 +4768,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4741,7 +4782,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4782,6 +4827,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11562,6 +11610,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19872,6 +19923,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 39eef1d6617..a9cbed8c9ce 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 538e7dcb493..3e5cea8384f 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 2485d8f360e..b7e9889d3f2 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3302,6 +3302,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..10b5f7f29cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6712,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6736,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 7840fdf62ea..08b9df5bc3b 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,11 +2266,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3581,6 +3586,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..a09f0f2ab99 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -162,8 +163,9 @@ typedef enum PublicationPartOpt
 } PublicationPartOpt;
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationExcludeRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e04f94b39f8..14b7ede4515 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -210,13 +210,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -235,8 +259,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1933,9 +1974,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1952,7 +1999,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1971,6 +2035,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1989,6 +2058,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2006,6 +2081,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2044,9 +2125,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 84aea7027a1..6e814edace6 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1228,17 +1241,31 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
+
 -- Verify that tables associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1246,6 +1273,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that schemas associated with the publication are dropped after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1253,6 +1283,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1260,6 +1294,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1276,10 +1314,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v16-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v16-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 8d397c42449c674583d69bcf23a1988233441b1a Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v16 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 108 ++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 315 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1bf7eaae5b3..4d1ed875849 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,91 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemas = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Drop the schemas associated with the publication */
+	schemas = GetPublicationSchemas(pubid);
+	PublicationDropSchemas(pubid, schemas, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Drop the relations associated with the publication */
+	PublicationDropTables(pubform->oid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1595,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 73345bb3c70..850d0fd2fd5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10825,6 +10825,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10871,6 +10873,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 37524364290..7840fdf62ea 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2263,7 +2263,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 3a2eacd793f..e04f94b39f8 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1930,6 +1930,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index c9e309190df..84aea7027a1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1225,6 +1225,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that tables associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that schemas associated with the publication are dropped after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v16-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v16-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 222ea85580a6c3beeaf47be3751dcf4753a747d2 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 18 Jul 2025 15:31:44 +0530
Subject: [PATCH v16 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

The column "prexcept" of system catalog "pg_publication_rel" is set to
"true" when publication is created with EXCEPT table or EXCEPT column
list. If column "prattrs" of system catalog "pg_publication_rel" is also
set or column "puballtables" of system catalog "pg_publication" is
"false", it indicates the column list is specified with EXCEPT clause
and columns in "prattrs" are excluded from being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 114 ++++++++++++----
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 ++++++--
 src/backend/catalog/pg_publication.c          |  61 ++++++++-
 src/backend/commands/publicationcmds.c        |  30 ++++-
 src/backend/parser/gram.y                     |  65 +++++++++
 src/backend/replication/pgoutput/pgoutput.c   |  61 ++++++++-
 src/bin/pg_dump/pg_dump.c                     |  45 ++++---
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 100 +++++++++-----
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  72 ++++++++++
 src/test/regress/sql/publication.sql          |  52 ++++++++
 .../t/036_rep_changes_except_table.pl         | 124 +++++++++++++++++-
 17 files changed, 694 insertions(+), 113 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1bb1db26045..b045d814f05 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 2e9f6019474..de32fd33a87 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Generated columns can be
+   specified in a column list using the <literal>EXCEPT</literal> clause. This
+   excludes the specified generated columns from being published, regardless of
+   the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1491,12 +1508,13 @@ Publications:
      for each publication.
 <programlisting>
 /* pub # */ \dRp+
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+ Owner  | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
+--------+------------+---------+---------+---------+-----------+-------------------+----------
+ ubuntu | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index bd25a1a723c..c8e9c4b216c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -259,6 +259,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index a2f9c0d4825..8ec266c9e97 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -335,10 +342,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -353,6 +362,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    system columns.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The generated columns that are part of <literal>REPLICA IDENTITY</literal>
    must be published explicitly either by listing them in the column list or
@@ -482,6 +501,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 1878fba8748..6bce5adc74e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -646,10 +661,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the exceptcols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -672,6 +689,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -776,8 +796,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -1288,6 +1310,9 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Datum		exceptDatum;
+		bool		isnull;
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1312,7 +1337,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
@@ -1321,6 +1345,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
 										&(nulls[3]));
+
+			/*
+			 * We fetch pubtuple if publication is not FOR ALL TABLES and not
+			 * FOR TABLES IN SCHEMA. So if prexcept is true, it indicates that
+			 * prattrs contains columns to be excluded for replication.
+			 */
+			exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+										  Anum_pg_publication_rel_prexcept,
+										  &isnull);
+
+			if (!isnull && DatumGetBool(exceptDatum) && !nulls[2])
+				except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
 		}
 		else
 		{
@@ -1328,8 +1364,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1360,6 +1400,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index d25de331c34..e9452748b2a 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,7 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ * 	  If any column is missing, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -381,6 +381,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +405,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +496,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1440,6 +1448,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1455,6 +1464,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		exceptDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1471,6 +1481,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull)
+					oldexcept = DatumGetBool(exceptDatum);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1502,7 +1519,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index bdbbcccd47f..95db9f12e3f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,6 +446,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list except_pub_obj_list
+				opt_except_column_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -4426,6 +4427,10 @@ opt_column_list:
 			| /*EMPTY*/								{ $$ = NIL; }
 		;
 
+opt_except_column_list:
+			'(' columnList ')'						{ $$ = $2; }
+		;
+
 columnList:
 			columnElem								{ $$ = list_make1($1); }
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
@@ -10744,6 +10749,17 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| TABLE relation_expr EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $2;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->pubtable->except = true;
+					$$->location = @1;
+				}
 			| TABLES IN_P SCHEMA ColId
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10784,6 +10800,34 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
+			| ColId EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					/*
+					 * If either a row filter or exclude column list is
+					 * specified, create a PublicationTable object.
+					 */
+					if ($3 || $4)
+					{
+						/*
+						 * The OptWhereClause must be stored here but it is
+						 * valid only for tables. For non-table objects, an
+						 * error will be thrown later via
+						 * preprocess_pubobj_list().
+						 */
+						$$->pubtable = makeNode(PublicationTable);
+						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
+						$$->pubtable->except = true;
+					}
+					else
+					{
+						$$->name = $1;
+					}
+					$$->location = @1;
+				}
 			| ColId indirection opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
@@ -10794,6 +10838,17 @@ PublicationObjSpec:
 					$$->pubtable->whereClause = $4;
 					$$->location = @1;
 				}
+			| ColId indirection EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
+					$$->pubtable->except = true;
+					$$->location = @1;
+				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
 			| extended_relation_expr opt_column_list OptWhereClause
 				{
@@ -10804,6 +10859,16 @@ PublicationObjSpec:
 					$$->pubtable->columns = $2;
 					$$->pubtable->whereClause = $3;
 				}
+			| extended_relation_expr EXCEPT opt_except_column_list OptWhereClause
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->relation = $1;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = true;
+				}
 			| CURRENT_SCHEMA
 				{
 					$$ = makeNode(PublicationObjSpec);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 08111b571de..d186564c297 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT clause in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'no_cols_published' flag is introduced.
+	 */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,11 +1141,30 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		no_col_published = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				no_col_published = true;
+		}
 
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
@@ -1134,7 +1172,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		 * of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!no_col_published && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1184,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1194,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->no_cols_published = no_col_published;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published != no_col_published) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1522,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2106,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2156,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 79747f9a99f..3a6eeee4c51 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4781,24 +4781,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4824,10 +4807,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4907,7 +4909,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index a9cbed8c9ce..3b3d867db58 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -682,6 +682,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 10b5f7f29cb..dff9a2a3006 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,14 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,37 +3040,61 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
-
-				appendPQExpBuffer(&buf,
+								  "WHERE pr.prrelid = '%s' "
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3106,8 +3132,15 @@ describeOneTableDetails(const char *schemaname,
 
 				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
+				{
+					if (!PQgetisnull(result, i, 3) &&
+						strcmp(PQgetvalue(result, i, 3), "t") == 0)
+						appendPQExpBuffer(&buf, " EXCEPT (%s)",
+										  PQgetvalue(result, i, 2));
+					else
+						appendPQExpBuffer(&buf, " (%s)",
+										  PQgetvalue(result, i, 2));
+				}
 
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
@@ -6523,7 +6556,11 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 							  PQgetvalue(res, i, 1));
 
 			if (!PQgetisnull(res, i, 3))
+			{
+				if (!PQgetisnull(res, i, 4) && strcmp(PQgetvalue(res, i, 4), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
 				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+			}
 
 			if (!PQgetisnull(res, i, 2))
 				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
@@ -6706,6 +6743,13 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6714,10 +6758,6 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			/* FIXME: 180000 should be changed to 190000 later for PG19. */
-			if (pset.sversion >= 180000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 08b9df5bc3b..d4c9152b1ff 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,6 +2269,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3594,7 +3596,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index a09f0f2ab99..ec52d23d776 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 14b7ede4515..33c8f56b65e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2132,6 +2132,78 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  syntax error at or near ";"
+LINE 1: ...BLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+                                                                      ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify that EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+ERROR:  index "pub_test_except1_a_idx" for table "pub_test_except1" does not exist
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify that so long as no clash between RI cols and the EXCEPT
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6e814edace6..ba57529e53f 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1322,6 +1322,58 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify that EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify that so long as no clash between RI cols and the EXCEPT
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
index 1d115283809..660467836a4 100644
--- a/src/test/subscription/t/036_rep_changes_except_table.pl
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -1,7 +1,7 @@
 
 # Copyright (c) 2021-2022, PostgreSQL Global Development Group
 
-# Logical replication tests for except table publications
+# Logical replication tests for except table and except column publications
 use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
@@ -77,6 +77,128 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*), min(a), max(a) FROM public.tab1");
 is($result, qq(0||), 'check rows on subscriber catchup');
 
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab5 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2 EXCEPT (b, c)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab5 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+	'check that initial sync for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is($result, qq(1||), 'check that initial sync for except column publication');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+# Test incremental changes
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is( $result, qq(1||
+4||), 'check incremental insert for except column publication');
+
+# Test for update
+$node_publisher->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_subscriber->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET a = 3, b = 4, c = 5 WHERE a = 1");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|5|6
+|4|5),
+	'check update for except column publication');
+
+# Test ALTER PUBLICATION for EXCEPT (col_list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab3 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3");
+is($result, qq(1||3), 'check alter publication with EXCEPT');
+
+# Test for publication created with publish_generated_columns as true on table
+# with generated columns and column list specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET (publish_generated_columns)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4");
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as true) with generated columns and EXCEPT'
+);
+
+# Test for publication created with publish_generated_columns as false on table
+# with generated columns and column list specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET (publish_generated_columns=none)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as false) with generated columns and EXCEPT'
+);
+
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
 
-- 
2.34.1

#94Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#91)
Re: Skipping schema changes in publication

On Mon, 30 Jun 2025 at 12:28, shveta malik <shveta.malik@gmail.com> wrote:

On Fri, Jun 27, 2025 at 3:44 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 26 Jun 2025 at 15:27, shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Jun 24, 2025 at 9:48 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have included the changes for
it in v14-0003 patch.

Thanks for the patches. I have reviewed patch001 alone, please find
few comments:

1)
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the
+   default state which includes resetting the publication parameters, setting
+   <literal>ALL TABLES</literal> flag to <literal>false</literal> and
+   dropping all relations and schemas that are associated with the
+   publication.
</para>

It is misleading, as far as I have understood, we do not drop the
tables or schemas associated with the pub; we just remove those from
the publication's object list. See previous doc:
"The ADD and DROP clauses will add and remove one or more
tables/schemas from the publication"

Perhaps we want to say the same thing when we speak about the 'drop'
aspect of RESET.

I have updated the document.

2)
AlterPublicationReset():

+ if (!OidIsValid(prid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("relation \"%s\" is not part of the publication",
+ get_rel_name(relid))));

Can you please help me understand which scenario will give this error?

Another question is do we really need this error? IIUC, we generally
give errors if a user has explicitly called out a name of an object
and that object is not found. Example:

postgres=# alter publication pubnew drop table t1,tab2;
ERROR: relation "t1" is not part of the publication

While in a few other cases, we pass missing_okay as true and do not
give errors. Please see other callers of performDeletion in
publicationcmds.c itself. There we have usage of missing_okay=true. I
have not researched myself, but please analyze the cases where
missing_okay is passed as true to figure out if those match our RESET
case. Try to reproduce if possible and then take a call.

I thought about the above point and I also think this check is not
required. Also, the function was calling PublicationDropSchemas with
missing_ok as false. I have changed it to be true.

Okay. Is there a reason for not using PublicationDropTables() here? We
have rewritten similar code in the Reset flow.

I feel it's better to use the function PublicationDropTables(). Also
proper locking would be required on tables while dropping them from
publication.
Made changes for the same.

3)
+ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA public;
+ERROR:  syntax error at or near "ALL"
+LINE 1: ALTER PUBLICATION testpub_reset ADD ALL TABLES IN SCHEMA pub...

There is a problem in syntax, I think the intention of testcase was to
run this query successfully.

I have fixed it.

Thanks Shveta for reviewing the patch. I have addressed the comments
and posted an updated version v15 in [1].

Thanks for the patches. My review is in progress but please find few
comments on 002:

1)
where exception_object is:
[ ONLY ] table_name [ * ]

We have the above in CREATE and ALTER pub docs, but we do not explain
ONLY with EXCEPT. We do have an explanation of ONLY under 'FOR TABLE'.
But since 'FOR TABLE' and 'EXCEPT' do not go together, it is somewhat
difficult to connect the dots and find the information ONLY in the
context of EXCEPT. We shall have ONLY explained for EXCEPT as well. Or
we can have ONLY defined in a way that both 'FOR TABLE' and 'EXCEPT'
can refer to it.

In create_publication.sgml, added it under "EXCEPT_TABLE'. In
alter_publication.sgml, modified the document under item 'table_name'
under "<title>Parameters</title>"

2)
We get tab-completion options in this command:
postgres=# create publication pub5 for TABLE tab1 W
WHERE ( WITH (

Similarly in this command:
create publication pub5 for TABLES IN SCHEMA s1

But once we have 'EXCEPT TABLE', we do not get further tab-completion
option like WITH(...)
create publication pub5 for ALL TABLES EXCEPT TABLE tab1

Fixed

3)
During tab-expansion, 'EXCEPT TABLE' and 'WITH (' in the below
command looks like they are connecting words. Can the gap be increased
similar to tab-expansion of next command shown below:

postgres=# create publication pub4 for ALL TABLES
EXCEPT TABLE WITH (

I did not find a place to add any custom space. It is default
behaviour to add 2 spaces between different words. See similar:
postgres=# CREATE PUBLICATION pub1 FOR TABLE t1 W
WHERE ( WITH (

postgres=# create publication pub4 for
ALL TABLES TABLE TABLES IN SCHEMA

I observed that the space between word is dependent on the length of
longest word. Here the longest word is "TABLES IN SCHEMA". The space
between the words are quite noticeable.

4)
alter_publication.sgml.orig is a left-over in patch002.

Fixed

I have added the changes in the latest v16 patch [1]/messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com.
[1]: /messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com

Thanks and Regards,
Shlok Kyal

#95Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#90)
Re: Skipping schema changes in publication

On Mon, 30 Jun 2025 at 11:54, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

One more thing, I noticed there is no tab-completion code yet for this
new EXCEPT (column_list) syntax.

I have added the tab-completion code in the latest v16 patch [1]/messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com.
[1]: /messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com

Thanks and Regards,
Shlok Kyal

#96Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#92)
Re: Skipping schema changes in publication

On Mon, 30 Jun 2025 at 16:25, shveta malik <shveta.malik@gmail.com> wrote:

Few more comments on 002:

5)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
{

+ List    *exceptlist;
+
+ exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);

a) Here, we are assuming that the list provided by
GetPublicationRelations() will be except-tables list only, but there
is no validation of that.
b) We are using GetPublicationRelations() to get the relations which
are excluded from the publication. The name of function and comments
atop function are not in alignment with this usage.

Suggestion:
We can have a new GetPublicationExcludeRelations() function for the
concerned usage. The existing logic of GetPublicationRelations() can
be shifted to a new internal-logic function which will accept a
'except-flag' as well. Both GetPublicationRelations() and
GetPublicationExcludeRelations() can call that new function by passing
'except-flag' as false and true respectively. The new internal
function will validate 'prexcept' against that except-flag passed and
will return the results.

I have made the above change.

6)
Before your patch002, GetTopMostAncestorInPublication() was checking
pg_publication_rel and pg_publication_namespace to find out if the
table in the ancestor-list is part of a given particular. Both
pg_publication_rel and pg_publication_namespace did not have the entry
"for all tables" publications. That means
GetTopMostAncestorInPublication() was originally not checking whether
the given puboid is an "for all tables" publication to see if a rel
belongs to that particular pub or not. I

But now with the current change, we do check if pub is all-tables pub,
if so, return relid and mark ancestor_level (provided table is not
part of the except list). IIUC, the result in 2 cases may be
different. Is that the intention? Let me know if my understanding is
wrong.

This is intentional, in function get_rel_sync_entry, we are setting
pub_relid to the topmost published ancestor. In HEAD we are directly
setting using:
/*
* If this is a FOR ALL TABLES publication, pick the partition
* root and set the ancestor level accordingly.
*/
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
In HEAD, we can directly use 'llast_oid(ancestors)' to get the topmost
ancestor for case of FOR ALL TABLES.
But with this proposal. This change will no longer be valid as the
'llast_oid(ancestors)' may be excluded in the publication. So, to
handle this change was made in GetTopMostAncestorInPublication.

Also, during testing with the partitioned table and
publish_via_partition_root the behaviour of the current patch is as
below:
For example we have a partitioned table t1. It has partitions part1
and part2. Now consider the following cases:
1. with publish_via_partition_root = true
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.
II. If we create publication on all tables with EXCEPT part1,
data for all tables t1, part1 and part2 is replicated.
2. with publish_via_partition_root = false
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.
II. If we create publication on all tables with EXCEPT part1,
data for part1 is not replicated

Is this behaviour fine?
I checked for other databases such as MySQL, SQL Server. In that we do
not have such cases as either we replicate the whole partitioned table
or we not replicated at all. We do not have partition level control.
For Oracle, I found that we can include or exclude partitions using
'PARTITIONEXCLUDE' [2]https://docs.oracle.com/en/middleware/goldengate/core/23/reference/partition-partitionexclude.html Thanks, Shlok Kyal, but did not find something similar to
publish_via_partition_root or where partitions are published as
separate tables.
What are your thoughts on the above behaviour?

I have addressed the comments and added the changes in the latest v16 patch [1]/messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com.
[1]: /messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com
[2]: https://docs.oracle.com/en/middleware/goldengate/core/23/reference/partition-partitionexclude.html Thanks, Shlok Kyal
Thanks,
Shlok Kyal

#97Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#93)
Re: Skipping schema changes in publication

Hi Shlok.

Some review comments for patch v16-0003.

======
Commit message

1.
The column "prexcept" of system catalog "pg_publication_rel" is set to
"true" when publication is created with EXCEPT table or EXCEPT column
list. If column "prattrs" of system catalog "pg_publication_rel" is also
set or column "puballtables" of system catalog "pg_publication" is
"false", it indicates the column list is specified with EXCEPT clause
and columns in "prattrs" are excluded from being published.

~

Somehow, this seems to contain too much information, making it a bit
confusing. Can't you chop this down to something like below?

SUGESTION
When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

======
doc/src/sgml/logical-replication.sgml

2.
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Generated
columns can be
+   specified in a column list using the <literal>EXCEPT</literal> clause. This
+   excludes the specified generated columns from being published, regardless of
+   the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>

~~

For this part:

"Generated columns can be specified in a column list using the
<literal>EXCEPT</literal> clause. This excludes the specified
generated columns from being published, regardless of..."

I think the whole paragraph already said "Generated columns can also
be specified in a column list", so you don't need to repeat it.
Instead, maybe say something like below.

SUGGESTION
Specifying generated columns in a column list using the
<literal>EXCEPT</literal> clause excludes those columns from being
published, regardless of...

~~~

3.
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+ Owner  | All tables | Inserts | Updates | Deletes | Truncates |
Generated columns | Via root
+--------+------------+---------+---------+---------+-----------+-------------------+----------
+ ubuntu | f          | t       | t       | t       | t         | none
             | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>

I noticed the Owner changed from "postgres" to "ubuntu". Do you think
it is better to keep this as "postgres" for the example?

======
doc/src/sgml/ref/create_publication.sgml

4.
The tables added to a publication that publishes UPDATE and/or DELETE
operations must have REPLICA IDENTITY defined. Otherwise those
operations will be disallowed on those tables.

In order for UPDATE or DELETE operations to work, all the REPLICA
IDENTITY columns must be published. So, any column list must name all
REPLICA IDENTITY columns, and any EXCEPT column list must not name any
REPLICA IDENTITY columns.

A row filter expression (i.e., the WHERE clause) must contain only
columns that are covered by the REPLICA IDENTITY, in order for UPDATE
and DELETE operations to be published. For publication of INSERT
operations, any column may be used in the WHERE expression. The row
filter allows simple expressions that don't have user-defined
functions, user-defined operators, user-defined types, user-defined
collations, non-immutable built-in functions, or references to system
columns.

The generated columns that are part of the column list specified with
the EXCEPT clause are not published, regardless of the
publish_generated_columns option. However, generated columns that are
not part of the column list specified with the EXCEPT clause are
published according to the value of the publish_generated_columns
option. See Section 29.6 for details.

The generated columns that are part of REPLICA IDENTITY must be
published explicitly either by listing them in the column list or by
enabling the publish_generated_columns option, in order for UPDATE and
DELETE operations to be published.

~~

Notice all those 5 paragraphs (above) are talking about REPLICA
IDENTITY, except the 4th paragraph. Maybe the 4th paragraph should be
moved to last, to keep all the REPLICA IDENTITY stuff together.

======
src/backend/catalog/pg_publication.c

5. pub_form_cols_map

  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the exceptcols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+   Bitmapset *except_cols)

Forgot to add the underscore in the function comment.

/exceptcols/except_cols/

~~~

6. pg_get_publication_tables

+
+ /*
+ * We fetch pubtuple if publication is not FOR ALL TABLES and not
+ * FOR TABLES IN SCHEMA. So if prexcept is true, it indicates that
+ * prattrs contains columns to be excluded for replication.
+ */
+ exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+   Anum_pg_publication_rel_prexcept,
+   &isnull);
+
+ if (!isnull && DatumGetBool(exceptDatum) && !nulls[2])
+ except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);

But, you cannot have EXCEPT for null column list, so shouldn't the
!nulls[2] check be done to also guard the SysCacheGetAttr call?

======
src/backend/parser/gram.y

7.

Shlok wrote [1-reply #11]
The main reason I used a separate 'opt_except_column_list' is because
'opt_column_list' can also be NULL. But the column list specified with
EXCEPT not be NULL. So, 'opt_except_column_list' is defined such that
it cannot be null.

~

Yeah, but IMO that leads to excessive duplicated code. I think the
code can perhaps be a lot simpler if the grammar is written more like
the synopsis:

e.g. TABLE name opt_EXCEPT opt_column_list

where - opt_EXCEPT is null, and opt_column_list is null... means no col list
where - opt_EXCEPT is null, and opt_column_list is not null... means
normal col list
where - opt_EXCEPT is not null, and opt_column_list not null... means
EXCEPT col list
where - opt_EXCEPT is not null, and opt_column_list null... SYNTAX ERROR

So code it something like this (just adding opt_EXCEPT to the existing
productions)

%type <boolean> opt_ordinality opt_without_overlaps opt_EXCEPT
...
opt_EXCEPT:
EXCEPT { $$ = true; }
| /*EMPTY*/ { $$ = false; }
;
...
TABLE relation_expr opt_EXCEPT opt_column_list OptWhereClause
{
$$ = makeNode(PublicationObjSpec);
$$->pubobjtype = PUBLICATIONOBJ_TABLE;
$$->pubtable = makeNode(PublicationTable);
$$->pubtable->relation = $2;
$$->pubtable->except = $3;
$$->pubtable->columns = $4;
if ($3 && !$4)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("EXCEPT without column list"),
parser_errposition(@3)));
$$->pubtable->whereClause = $5;
$$->location = @1;
}

etc.

======
src/bin/psql/describe.c

8.
  if (!PQgetisnull(res, i, 3))
+ {
+ if (!PQgetisnull(res, i, 4) && strcmp(PQgetvalue(res, i, 4), "t") == 0)
+ appendPQExpBuffer(buf, " EXCEPT");
  appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+ }

This growing list of columns makes it hard to understand this function
without looking back at the caller all the time. Maybe you can add a
function comment that at least explains what those attributes 1,2,3,4
represent?

======
src/bin/psql/tab-complete.in.c

9.
+ else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET",
"TABLE", MatchAny))
+ COMPLETE_WITH("EXCEPT");

Since it is not allowed to have an EXCEPT with no column list,
shouldn't this say "EXCEPT ("?

~~~

10.
  else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE",
MatchAny) && !ends_with(prev_wd, ','))
- COMPLETE_WITH("WHERE (", "WITH (");
+ COMPLETE_WITH("EXCEPT", "WHERE (", "WITH (");

Ditto. Since it is not allowed to have an EXCEPT with no column list,
shouldn't this say "EXCEPT ("?

======
src/test/regress/expected/publication.out

11.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+ERROR:  index "pub_test_except1_a_idx" for table "pub_test_except1"
does not exist
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the
replica identity.
+DROP INDEX pub_test_except1_ac_idx;

What's happening here? I'm not sure these are the kind of errors you
were trying to cause.

======
src/test/regress/sql/publication.sql

12.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;

SUGGESTION. Change that comment to:
Verify fails - EXCEPT col-list cannot...

~~~

13.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;

SUGGESTION. Change that comment to:
Verify fails - EXCEPT col-list cannot...

~~~

14.
+-- Verify that so long as no clash between RI cols and the EXCEPT
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+

That comment doesn't make sense. Missing words?

======
.../t/036_rep_changes_except_table.pl

15.
(I haven't reviewed this file in detail yet, but here is a general comment)

I know this patch currently lives in the same thread as all the EXCEPT
TABLE stuff, but that seems just happenstance to me. IMO, this is a
separate enhancement that just shares the keyword EXCEPT. So, I felt
it should have quite separate tests too.

e.g. How about: 037_rep_changes_except_collist.pl

======
[1]: /messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#98shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#96)
Re: Skipping schema changes in publication

On Sat, Jul 19, 2025 at 4:17 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 30 Jun 2025 at 16:25, shveta malik <shveta.malik@gmail.com> wrote:

Few more comments on 002:

5)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
{

+ List    *exceptlist;
+
+ exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);

a) Here, we are assuming that the list provided by
GetPublicationRelations() will be except-tables list only, but there
is no validation of that.
b) We are using GetPublicationRelations() to get the relations which
are excluded from the publication. The name of function and comments
atop function are not in alignment with this usage.

Suggestion:
We can have a new GetPublicationExcludeRelations() function for the
concerned usage. The existing logic of GetPublicationRelations() can
be shifted to a new internal-logic function which will accept a
'except-flag' as well. Both GetPublicationRelations() and
GetPublicationExcludeRelations() can call that new function by passing
'except-flag' as false and true respectively. The new internal
function will validate 'prexcept' against that except-flag passed and
will return the results.

I have made the above change.

6)
Before your patch002, GetTopMostAncestorInPublication() was checking
pg_publication_rel and pg_publication_namespace to find out if the
table in the ancestor-list is part of a given particular. Both
pg_publication_rel and pg_publication_namespace did not have the entry
"for all tables" publications. That means
GetTopMostAncestorInPublication() was originally not checking whether
the given puboid is an "for all tables" publication to see if a rel
belongs to that particular pub or not. I

But now with the current change, we do check if pub is all-tables pub,
if so, return relid and mark ancestor_level (provided table is not
part of the except list). IIUC, the result in 2 cases may be
different. Is that the intention? Let me know if my understanding is
wrong.

This is intentional, in function get_rel_sync_entry, we are setting
pub_relid to the topmost published ancestor. In HEAD we are directly
setting using:
/*
* If this is a FOR ALL TABLES publication, pick the partition
* root and set the ancestor level accordingly.
*/
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
In HEAD, we can directly use 'llast_oid(ancestors)' to get the topmost
ancestor for case of FOR ALL TABLES.
But with this proposal. This change will no longer be valid as the
'llast_oid(ancestors)' may be excluded in the publication. So, to
handle this change was made in GetTopMostAncestorInPublication.

Also, during testing with the partitioned table and
publish_via_partition_root the behaviour of the current patch is as
below:
For example we have a partitioned table t1. It has partitions part1
and part2. Now consider the following cases:
1. with publish_via_partition_root = true
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.
II. If we create publication on all tables with EXCEPT part1,
data for all tables t1, part1 and part2 is replicated.
2. with publish_via_partition_root = false
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.
II. If we create publication on all tables with EXCEPT part1,
data for part1 is not replicated

Is this behaviour fine?
I checked for other databases such as MySQL, SQL Server. In that we do
not have such cases as either we replicate the whole partitioned table
or we not replicated at all. We do not have partition level control.
For Oracle, I found that we can include or exclude partitions using
'PARTITIONEXCLUDE' [2], but did not find something similar to
publish_via_partition_root or where partitions are published as
separate tables.
What are your thoughts on the above behaviour?

Thank You for the details. I will review this behaviour soon and will
let you know my comments. Meanwhile, please find a few comments on
v16-0001:

1)
we do LockSchemaList() everywhere before we call
PublicationDropSchemas() to prevent concurrent schema deletion. Do we
need that in reset flow as well?

2)
+ /* Drop the schemas associated with the publication */
+ schemas = GetPublicationSchemas(pubid);
+ PublicationDropSchemas(pubid, schemas, true);
+
+ /* Get all relations associated with the publication */
+ relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);

We can rename schemas to schemaids similar to relids, as
GetPublicationSchemas return oids.

3)
+ /* Drop the relations associated with the publication */
+ PublicationDropTables(pubform->oid, rels, true);

we can pass 'pubid' here instead of pubform->oid

4)
Shall we modify the comments:
'Drop the relations associated with the publication' to 'Remove the
associated relations from the publication'
'Drop the schemas associated with the publication' to 'Remove the
associated schemas from the publication'

Similar changes can be done in test file's comments as well
--Verify that tables associated with the publication are dropped after
RESET
--Verify that schemas associated with the publication are dropped after RESET

thanks
Shveta

#99Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#97)
3 attachment(s)
Re: Skipping schema changes in publication

On Mon, 21 Jul 2025 at 12:17, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Some review comments for patch v16-0003.

======
Commit message

1.
The column "prexcept" of system catalog "pg_publication_rel" is set to
"true" when publication is created with EXCEPT table or EXCEPT column
list. If column "prattrs" of system catalog "pg_publication_rel" is also
set or column "puballtables" of system catalog "pg_publication" is
"false", it indicates the column list is specified with EXCEPT clause
and columns in "prattrs" are excluded from being published.

~

Somehow, this seems to contain too much information, making it a bit
confusing. Can't you chop this down to something like below?

SUGESTION
When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

Modified the commit message as per suggestion.

======
doc/src/sgml/logical-replication.sgml

2.
Generated columns can also be specified in a column list. This allows
generated columns to be published, regardless of the publication parameter
<link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Generated
columns can be
+   specified in a column list using the <literal>EXCEPT</literal> clause. This
+   excludes the specified generated columns from being published, regardless of
+   the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
<literal>publish_generated_columns</literal></link>. See
<xref linkend="logical-replication-gencols"/> for details.
</para>

~~

For this part:

"Generated columns can be specified in a column list using the
<literal>EXCEPT</literal> clause. This excludes the specified
generated columns from being published, regardless of..."

I think the whole paragraph already said "Generated columns can also
be specified in a column list", so you don't need to repeat it.
Instead, maybe say something like below.

SUGGESTION
Specifying generated columns in a column list using the
<literal>EXCEPT</literal> clause excludes those columns from being
published, regardless of...

~~~

Modified

3.
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+ Owner  | All tables | Inserts | Updates | Deletes | Truncates |
Generated columns | Via root
+--------+------------+---------+---------+---------+-----------+-------------------+----------
+ ubuntu | f          | t       | t       | t       | t         | none
| f
Tables:
"public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
</programlisting></para>

I noticed the Owner changed from "postgres" to "ubuntu". Do you think
it is better to keep this as "postgres" for the example?

I agree that it is better to keep "postgres". I have reverted back to
the use "postgres"..

======
doc/src/sgml/ref/create_publication.sgml

4.
The tables added to a publication that publishes UPDATE and/or DELETE
operations must have REPLICA IDENTITY defined. Otherwise those
operations will be disallowed on those tables.

In order for UPDATE or DELETE operations to work, all the REPLICA
IDENTITY columns must be published. So, any column list must name all
REPLICA IDENTITY columns, and any EXCEPT column list must not name any
REPLICA IDENTITY columns.

A row filter expression (i.e., the WHERE clause) must contain only
columns that are covered by the REPLICA IDENTITY, in order for UPDATE
and DELETE operations to be published. For publication of INSERT
operations, any column may be used in the WHERE expression. The row
filter allows simple expressions that don't have user-defined
functions, user-defined operators, user-defined types, user-defined
collations, non-immutable built-in functions, or references to system
columns.

The generated columns that are part of the column list specified with
the EXCEPT clause are not published, regardless of the
publish_generated_columns option. However, generated columns that are
not part of the column list specified with the EXCEPT clause are
published according to the value of the publish_generated_columns
option. See Section 29.6 for details.

The generated columns that are part of REPLICA IDENTITY must be
published explicitly either by listing them in the column list or by
enabling the publish_generated_columns option, in order for UPDATE and
DELETE operations to be published.

~~

Notice all those 5 paragraphs (above) are talking about REPLICA
IDENTITY, except the 4th paragraph. Maybe the 4th paragraph should be
moved to last, to keep all the REPLICA IDENTITY stuff together.

Fixed

======
src/backend/catalog/pg_publication.c

5. pub_form_cols_map

* Returns a bitmap representing the columns of the specified table.
*
* Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the exceptcols are excluded from
+ * the column list.
*/
Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+   Bitmapset *except_cols)

Forgot to add the underscore in the function comment.

/exceptcols/except_cols/

Fixed

~~~

6. pg_get_publication_tables

+
+ /*
+ * We fetch pubtuple if publication is not FOR ALL TABLES and not
+ * FOR TABLES IN SCHEMA. So if prexcept is true, it indicates that
+ * prattrs contains columns to be excluded for replication.
+ */
+ exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+   Anum_pg_publication_rel_prexcept,
+   &isnull);
+
+ if (!isnull && DatumGetBool(exceptDatum) && !nulls[2])
+ except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);

But, you cannot have EXCEPT for null column list, so shouldn't the
!nulls[2] check be done to also guard the SysCacheGetAttr call?

Fixed

======
src/backend/parser/gram.y

7.

Shlok wrote [1-reply #11]
The main reason I used a separate 'opt_except_column_list' is because
'opt_column_list' can also be NULL. But the column list specified with
EXCEPT not be NULL. So, 'opt_except_column_list' is defined such that
it cannot be null.

~

Yeah, but IMO that leads to excessive duplicated code. I think the
code can perhaps be a lot simpler if the grammar is written more like
the synopsis:

e.g. TABLE name opt_EXCEPT opt_column_list

where - opt_EXCEPT is null, and opt_column_list is null... means no col list
where - opt_EXCEPT is null, and opt_column_list is not null... means
normal col list
where - opt_EXCEPT is not null, and opt_column_list not null... means
EXCEPT col list
where - opt_EXCEPT is not null, and opt_column_list null... SYNTAX ERROR

So code it something like this (just adding opt_EXCEPT to the existing
productions)

%type <boolean> opt_ordinality opt_without_overlaps opt_EXCEPT
...
opt_EXCEPT:
EXCEPT { $$ = true; }
| /*EMPTY*/ { $$ = false; }
;
...
TABLE relation_expr opt_EXCEPT opt_column_list OptWhereClause
{
$$ = makeNode(PublicationObjSpec);
$$->pubobjtype = PUBLICATIONOBJ_TABLE;
$$->pubtable = makeNode(PublicationTable);
$$->pubtable->relation = $2;
$$->pubtable->except = $3;
$$->pubtable->columns = $4;
if ($3 && !$4)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("EXCEPT without column list"),
parser_errposition(@3)));
$$->pubtable->whereClause = $5;
$$->location = @1;
}

etc.

I have modified it. I have created a function 'check_except_collist'
to throw error, to avoid duplication code for error message.

======
src/bin/psql/describe.c

8.
if (!PQgetisnull(res, i, 3))
+ {
+ if (!PQgetisnull(res, i, 4) && strcmp(PQgetvalue(res, i, 4), "t") == 0)
+ appendPQExpBuffer(buf, " EXCEPT");
appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+ }

This growing list of columns makes it hard to understand this function
without looking back at the caller all the time. Maybe you can add a
function comment that at least explains what those attributes 1,2,3,4
represent?

Added a comment

======
src/bin/psql/tab-complete.in.c

9.
+ else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET",
"TABLE", MatchAny))
+ COMPLETE_WITH("EXCEPT");

Since it is not allowed to have an EXCEPT with no column list,
shouldn't this say "EXCEPT ("?

Fixed

~~~

10.
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE",
MatchAny) && !ends_with(prev_wd, ','))
- COMPLETE_WITH("WHERE (", "WITH (");
+ COMPLETE_WITH("EXCEPT", "WHERE (", "WITH (");

Ditto. Since it is not allowed to have an EXCEPT with no column list,
shouldn't this say "EXCEPT ("?

Fixed

======
src/test/regress/expected/publication.out

11.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+ERROR:  index "pub_test_except1_a_idx" for table "pub_test_except1"
does not exist
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the
replica identity.
+DROP INDEX pub_test_except1_ac_idx;

What's happening here? I'm not sure these are the kind of errors you
were trying to cause.

Yes, it is not the error I was trying to cause. I have modified it.

======
src/test/regress/sql/publication.sql

12.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;

SUGGESTION. Change that comment to:
Verify fails - EXCEPT col-list cannot...

Fixed

~~~

13.
+-- Verify that EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;

SUGGESTION. Change that comment to:
Verify fails - EXCEPT col-list cannot...

Fixed

~~~

14.
+-- Verify that so long as no clash between RI cols and the EXCEPT
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX
pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+

That comment doesn't make sense. Missing words?

Fixed

======
.../t/036_rep_changes_except_table.pl

15.
(I haven't reviewed this file in detail yet, but here is a general comment)

I know this patch currently lives in the same thread as all the EXCEPT
TABLE stuff, but that seems just happenstance to me. IMO, this is a
separate enhancement that just shares the keyword EXCEPT. So, I felt
it should have quite separate tests too.

e.g. How about: 037_rep_changes_except_collist.pl

Modified

======
[1] /messages/by-id/CANhcyEW2LK4diNeCG862DE40yQoV3VAgf59kXUq2TuR8fnw5vQ@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v17-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v17-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 81c4f280c90bbf69e7fe0d396d22e540daff6100 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v17 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1bf7eaae5b3..c3af10c4dc6 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1603,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 73345bb3c70..850d0fd2fd5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10825,6 +10825,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10871,6 +10873,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 37524364290..7840fdf62ea 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2263,7 +2263,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 3a2eacd793f..34cecafb4f5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1930,6 +1930,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index c9e309190df..5a2300779eb 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1225,6 +1225,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v17-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v17-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From e747b5357c13883b92f95bf5136d623fc5f0b7dc Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 18 Jul 2025 15:31:44 +0530
Subject: [PATCH v17 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 114 ++++++++++---
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 ++++--
 src/backend/catalog/pg_publication.c          |  66 +++++++-
 src/backend/commands/publicationcmds.c        |  30 +++-
 src/backend/parser/gram.y                     |  58 +++++--
 src/backend/replication/pgoutput/pgoutput.c   |  61 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +++---
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 109 +++++++++----
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  71 +++++++++
 src/test/regress/sql/publication.sql          |  52 ++++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 150 ++++++++++++++++++
 18 files changed, 714 insertions(+), 126 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1bb1db26045..b045d814f05 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 2e9f6019474..5b3ed13a423 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns in a column list using the <literal>EXCEPT</literal> clause excludes
+   the specified generated columns from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1491,12 +1508,13 @@ Publications:
      for each publication.
 <programlisting>
 /* pub # */ \dRp+
-                               Publication p1
-  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root
-----------+------------+---------+---------+---------+-----------+----------
- postgres | f          | t       | t       | t       | t         | f
+                                        Publication p1
+  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
+----------+------------+---------+---------+---------+-----------+-------------------+----------
+ postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index bd25a1a723c..c8e9c4b216c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -259,6 +259,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index a2f9c0d4825..869c9f2ed75 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -335,10 +342,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -361,6 +370,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -482,6 +501,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 1878fba8748..40351c0594a 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -646,10 +661,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -672,6 +689,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -776,8 +796,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -1288,6 +1310,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1312,7 +1335,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
@@ -1321,6 +1343,25 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
 										&(nulls[3]));
+
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
 		}
 		else
 		{
@@ -1328,8 +1369,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1360,6 +1405,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0d5999f3307..dee1186229f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,7 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ * 	  If any column is missing, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -381,6 +381,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +405,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +496,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1448,6 +1456,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1463,6 +1472,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		exceptDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1479,6 +1489,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull)
+					oldexcept = DatumGetBool(exceptDatum);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1510,7 +1527,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index bdbbcccd47f..df5ecfedbdf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -204,6 +204,8 @@ static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static void check_except_collist(bool except, List* columns, int location,
+								 core_yyscan_t yyscanner);
 
 %}
 
@@ -526,7 +528,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4421,6 +4423,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10734,14 +10741,16 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					check_except_collist($3, $4, @3, yyscanner);
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10757,7 +10766,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10765,7 +10774,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10775,8 +10784,10 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						check_except_collist($2, $3, @2, yyscanner);
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10784,25 +10795,29 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					check_except_collist($3, $4, @3, yyscanner);
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					check_except_collist($2, $3, @2, yyscanner);
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19819,6 +19834,21 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
 	return (Node *) s;
 }
 
+/*
+ * Throw an error if no column list is specified with EXCEPT clause
+ */
+void
+check_except_collist(bool except, List* columns, int location,
+					 core_yyscan_t yyscanner)
+{
+	if (except && columns == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("EXCEPT clause cannot be used without column list"),
+				 parser_errposition(location)));
+}
+
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 08111b571de..d186564c297 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT clause in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'no_cols_published' flag is introduced.
+	 */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,11 +1141,30 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		no_col_published = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				no_col_published = true;
+		}
 
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
@@ -1134,7 +1172,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		 * of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!no_col_published && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1184,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1194,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->no_cols_published = no_col_published;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published != no_col_published) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1522,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2106,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2156,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f085338b322..4a600f4206a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4846,24 +4846,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4889,10 +4872,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4972,7 +4974,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index a9cbed8c9ce..3b3d867db58 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -682,6 +682,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 10b5f7f29cb..b4d9c1f2b21 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,14 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,37 +3040,61 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
-
-				appendPQExpBuffer(&buf,
+								  "WHERE pr.prrelid = '%s' "
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				/* FIXME: 180000 should be changed to 190000 later for PG19. */
-				if (pset.sversion >= 180000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3106,8 +3132,15 @@ describeOneTableDetails(const char *schemaname,
 
 				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
+				{
+					if (!PQgetisnull(result, i, 3) &&
+						strcmp(PQgetvalue(result, i, 3), "t") == 0)
+						appendPQExpBuffer(&buf, " EXCEPT (%s)",
+										  PQgetvalue(result, i, 2));
+					else
+						appendPQExpBuffer(&buf, " (%s)",
+										  PQgetvalue(result, i, 2));
+				}
 
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
@@ -6513,6 +6546,15 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 	if (count > 0)
 		printTableAddFooter(cont, footermsg);
 
+	/*---------------------------------------------------
+	 * Publication description columns:
+	 * [0]: schema name (nspname)
+	 * [1]: table name (relname)
+	 * [2]: row filter expression (prqual), may be NULL
+	 * [3]: column list (comma-separated), may be NULL
+	 * [4]: except flag ("t" if EXCEPT, else "f")
+	 *---------------------------------------------------
+	 */
 	for (i = 0; i < count; i++)
 	{
 		if (as_schema)
@@ -6523,7 +6565,11 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 							  PQgetvalue(res, i, 1));
 
 			if (!PQgetisnull(res, i, 3))
+			{
+				if (!PQgetisnull(res, i, 4) && strcmp(PQgetvalue(res, i, 4), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
 				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+			}
 
 			if (!PQgetisnull(res, i, 2))
 				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
@@ -6706,6 +6752,13 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6714,10 +6767,6 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			/* FIXME: 180000 should be changed to 190000 later for PG19. */
-			if (pset.sversion >= 180000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 08b9df5bc3b..7b2c252cdd7 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,6 +2269,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3594,7 +3596,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index a09f0f2ab99..ec52d23d776 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e53cadf5c2b..5f1954fdcc9 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2132,6 +2132,77 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause cannot be used without column list
+LINE 1: ...BLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+                                                                ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 99e1f553d65..feb97cf5184 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1321,6 +1321,58 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..2d7b7a0412a
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,150 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab5 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2 EXCEPT (b, c)"
+);
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab3 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab4 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab5 (a int, b int, c int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+	'check that initial sync for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is($result, qq(1||), 'check that initial sync for except column publication');
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+# Test incremental changes
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for except column publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab2");
+is( $result, qq(1||
+4||), 'check incremental insert for except column publication');
+
+# Test for update
+$node_publisher->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_subscriber->safe_psql('postgres',
+	"CREATE UNIQUE INDEX b_idx ON tab2 (b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE tab2 REPLICA IDENTITY USING INDEX b_idx");
+$node_publisher->safe_psql('postgres',
+	"UPDATE tab2 SET a = 3, b = 4, c = 5 WHERE a = 1");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is( $result, qq(|5|6
+|4|5),
+	'check update for except column publication');
+
+# Test ALTER PUBLICATION for EXCEPT (col_list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab3 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3");
+is($result, qq(1||3), 'check alter publication with EXCEPT');
+
+# Test for publication created with publish_generated_columns as true on table
+# with generated columns and column list specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET (publish_generated_columns)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4");
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as true) with generated columns and EXCEPT'
+);
+
+# Test for publication created with publish_generated_columns as false on table
+# with generated columns and column list specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET (publish_generated_columns=none)");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as false) with generated columns and EXCEPT'
+);
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
\ No newline at end of file
-- 
2.34.1

v17-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v17-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From bebf871c1066e1d657c20f742f4be36cfb54e37d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v17 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  21 +-
 doc/src/sgml/ref/create_publication.sgml      |  37 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          | 103 ++++++---
 src/backend/commands/publicationcmds.c        | 198 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  62 +++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |   8 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         |  83 ++++++++
 25 files changed, 730 insertions(+), 133 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0d23bc1b122..1bb1db26045 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index e26f7f59d4a..2e9f6019474 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2283,10 +2283,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..bd25a1a723c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected.
      </para>
 
      <para>
@@ -237,6 +244,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..a2f9c0d4825 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitions of
+      a partitioned table are always implicitly considered part of the
+      publication, so they are never explicitly excluded from the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +466,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 4f7b11175c6..cb4215071d0 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..1878fba8748 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -479,6 +492,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +761,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +777,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +787,16 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication. If except_table is true, the
+ * list contains relations oids that excluded from publication, else the list
+ * contains the relation oids that are part of publication.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
-List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+static List *
+GetPubIncludedOrExcludedRels(Oid pubid, PublicationPartOpt pub_partopt,
+							 bool except_table)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +821,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if ((except_table && pubrel->prexcept) || !except_table)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -816,6 +838,25 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 	return result;
 }
 
+/*
+ * Gets list of relation oids for a publication.
+ *
+ * This should only be used FOR TABLE publications, the FOR ALL TABLES
+ * should use GetAllTablesPublicationRelations().
+ */
+List *
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	return GetPubIncludedOrExcludedRels(pubid, pub_partopt, false);
+}
+
+/* Get list of relation oids excluded from the publication */
+List *
+GetPublicationExcludeRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	return GetPubIncludedOrExcludedRels(pubid, pub_partopt, true);
+}
+
 /*
  * Gets list of publication oids for publications marked as FOR ALL TABLES.
  */
@@ -861,13 +902,16 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationExcludeRelations(pubid, pubviaroot ? PUBLICATION_PART_ALL : PUBLICATION_PART_ROOT);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +928,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +950,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1206,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index c3af10c4dc6..0d5999f3307 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1289,6 +1315,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1428,6 +1527,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1601,6 +1701,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1813,6 +1927,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1885,6 +2000,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1960,8 +2076,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..47916ef32ae 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 850d0fd2fd5..bdbbcccd47f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -445,7 +445,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10677,7 +10678,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10697,12 +10698,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10740,6 +10742,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10815,6 +10818,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10827,6 +10849,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10853,6 +10877,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index f4c977262c5..08111b571de 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..5d55f1f4ece 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 604fc109416..f085338b322 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -185,6 +185,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4577,8 +4579,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4744,6 +4772,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4755,8 +4784,17 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		/* FIXME: 180000 should be changed to 190000 later for PG19. */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4767,6 +4805,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4782,6 +4821,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4793,6 +4833,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4806,7 +4847,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4847,6 +4892,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11642,6 +11690,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -19952,6 +20003,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 39eef1d6617..a9cbed8c9ce 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 538e7dcb493..3e5cea8384f 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -1498,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d8330e2bd17..b6feab980a3 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3304,6 +3304,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..10b5f7f29cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,36 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				/* FIXME: 180000 should be changed to 190000 later for PG19. */
+				if (pset.sversion >= 180000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6712,13 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6736,24 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			/* FIXME: 180000 should be changed to 190000 later for PG19. */
+			if (pset.sversion >= 180000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 7840fdf62ea..08b9df5bc3b 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,11 +2266,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3581,6 +3586,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..a09f0f2ab99 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -162,8 +163,9 @@ typedef enum PublicationPartOpt
 } PublicationPartOpt;
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationExcludeRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 34cecafb4f5..e53cadf5c2b 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -210,13 +210,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -235,8 +259,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1933,9 +1974,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1952,7 +1999,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1971,6 +2035,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1989,6 +2058,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2006,6 +2081,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2044,9 +2125,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 5a2300779eb..99e1f553d65 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1228,17 +1241,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1246,6 +1272,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1253,6 +1282,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1260,6 +1293,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1276,10 +1313,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..1d115283809
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,83 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a");
+$node_publisher->safe_psql('postgres', "CREATE TABLE public.tab1(a int)");
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE sch1.tab1 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE public.tab1 (a int)");
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema RESET");
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1"
+);
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO public.tab1 VALUES(generate_series(1,10))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#100Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#98)
Re: Skipping schema changes in publication

On Mon, 21 Jul 2025 at 16:22, shveta malik <shveta.malik@gmail.com> wrote:

On Sat, Jul 19, 2025 at 4:17 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 30 Jun 2025 at 16:25, shveta malik <shveta.malik@gmail.com> wrote:

Few more comments on 002:

5)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
{

+ List    *exceptlist;
+
+ exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);

a) Here, we are assuming that the list provided by
GetPublicationRelations() will be except-tables list only, but there
is no validation of that.
b) We are using GetPublicationRelations() to get the relations which
are excluded from the publication. The name of function and comments
atop function are not in alignment with this usage.

Suggestion:
We can have a new GetPublicationExcludeRelations() function for the
concerned usage. The existing logic of GetPublicationRelations() can
be shifted to a new internal-logic function which will accept a
'except-flag' as well. Both GetPublicationRelations() and
GetPublicationExcludeRelations() can call that new function by passing
'except-flag' as false and true respectively. The new internal
function will validate 'prexcept' against that except-flag passed and
will return the results.

I have made the above change.

6)
Before your patch002, GetTopMostAncestorInPublication() was checking
pg_publication_rel and pg_publication_namespace to find out if the
table in the ancestor-list is part of a given particular. Both
pg_publication_rel and pg_publication_namespace did not have the entry
"for all tables" publications. That means
GetTopMostAncestorInPublication() was originally not checking whether
the given puboid is an "for all tables" publication to see if a rel
belongs to that particular pub or not. I

But now with the current change, we do check if pub is all-tables pub,
if so, return relid and mark ancestor_level (provided table is not
part of the except list). IIUC, the result in 2 cases may be
different. Is that the intention? Let me know if my understanding is
wrong.

This is intentional, in function get_rel_sync_entry, we are setting
pub_relid to the topmost published ancestor. In HEAD we are directly
setting using:
/*
* If this is a FOR ALL TABLES publication, pick the partition
* root and set the ancestor level accordingly.
*/
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
In HEAD, we can directly use 'llast_oid(ancestors)' to get the topmost
ancestor for case of FOR ALL TABLES.
But with this proposal. This change will no longer be valid as the
'llast_oid(ancestors)' may be excluded in the publication. So, to
handle this change was made in GetTopMostAncestorInPublication.

Also, during testing with the partitioned table and
publish_via_partition_root the behaviour of the current patch is as
below:
For example we have a partitioned table t1. It has partitions part1
and part2. Now consider the following cases:
1. with publish_via_partition_root = true
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.
II. If we create publication on all tables with EXCEPT part1,
data for all tables t1, part1 and part2 is replicated.
2. with publish_via_partition_root = false
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.
II. If we create publication on all tables with EXCEPT part1,
data for part1 is not replicated

Is this behaviour fine?
I checked for other databases such as MySQL, SQL Server. In that we do
not have such cases as either we replicate the whole partitioned table
or we not replicated at all. We do not have partition level control.
For Oracle, I found that we can include or exclude partitions using
'PARTITIONEXCLUDE' [2], but did not find something similar to
publish_via_partition_root or where partitions are published as
separate tables.
What are your thoughts on the above behaviour?

Thank You for the details. I will review this behaviour soon and will
let you know my comments. Meanwhile, please find a few comments on
v16-0001:

1)
we do LockSchemaList() everywhere before we call
PublicationDropSchemas() to prevent concurrent schema deletion. Do we
need that in reset flow as well?

Added

2)
+ /* Drop the schemas associated with the publication */
+ schemas = GetPublicationSchemas(pubid);
+ PublicationDropSchemas(pubid, schemas, true);
+
+ /* Get all relations associated with the publication */
+ relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);

We can rename schemas to schemaids similar to relids, as
GetPublicationSchemas return oids.

Fixed

3)
+ /* Drop the relations associated with the publication */
+ PublicationDropTables(pubform->oid, rels, true);

we can pass 'pubid' here instead of pubform->oid

Modified

4)
Shall we modify the comments:
'Drop the relations associated with the publication' to 'Remove the
associated relations from the publication'
'Drop the schemas associated with the publication' to 'Remove the
associated schemas from the publication'

Similar changes can be done in test file's comments as well
--Verify that tables associated with the publication are dropped after
RESET
--Verify that schemas associated with the publication are dropped after RESET

Fixed

I have made the changes in the latest v17 patch [1]/messages/by-id/CANhcyEUtYV-9ujtxLasnxN_peT+3LuZjcRx1xUECh1CCmANB8w@mail.gmail.com.
[1]: /messages/by-id/CANhcyEUtYV-9ujtxLasnxN_peT+3LuZjcRx1xUECh1CCmANB8w@mail.gmail.com

Thanks,
Shlok Kyal

#101Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#99)
Re: Skipping schema changes in publication

Hi Shlok,

Some review comments for patch v17-0003. I also checked the TAP test this time.

======
doc/src/sgml/logical-replication.sgml

1.
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns in a column list using the <literal>EXCEPT</literal> clause excludes
+   the specified generated columns from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for

I think that is not quite the same wording I had previously suggested.
It sounds a bit odd/redundant saying "Specifying" and "specified" in
the same sentence.

======
src/backend/parser/gram.y

2. check_except_collist

I'm wondering if this checking should be done within the existing
preprocess_pubobj_list() function, alongside all the other ERROR
checking. Care needs to be taken to make sure the pubtable->except is
referring to an EXCEPT (col-list), instead of the other kind of EXCEPT
tables, but in general I think it is better to keep all the
publication combinations checking errors like this in one place.

======
src/bin/psql/describe.c

3. addFooterToPublicationDesc

- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (!PQgetisnull(result, i, 3) &&
+ strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT (%s)",
+   PQgetvalue(result, i, 2));
+ else
+ appendPQExpBuffer(&buf, " (%s)",
+   PQgetvalue(result, i, 2));
+ }

Do you really need to check !PQgetisnull(result, i, 3) here? (e.g.
The comment does not say that this attribute can be NULL)

======
.../t/037_rep_changes_except_collist.pl

4.
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications

Comment is wrong. These tests are for EXCEPT (column-list)

~~~

5.
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab3 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab4 (a int, b int GENERATED ALWAYS AS (a * 2) STORED,
c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab5 (a int, b int GENERATED ALWAYS AS (a * 2) STORED,
c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+ "INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2
EXCEPT (b, c)"
+);

5a.
I think you don't need to say "Test for except column publications",
because that is the purpose of thie entire file.

~

5b.
You can combine multiple of these safe_psql calls together

~

5c.
It might help make tests easier to read if you named those generated
columns 'b', 'c' cols as 'bgen', 'cgen' instead.

~

5d.
The table names are strange, because why does it start at tab2 when
there is not a tab1?

~~~

6.
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab3 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab4 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab5 (a int, b int, c int)");

You can combine multiple of these safe_psql calls together

~~~

7.
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+ 'check that initial sync for except column publication');

The message seems strange. Do you mean "check initial sync for an
'EXCEPT (column-list)' publication"

NOTE: There are many other messages where you wrote "for except column
publication" but I think maybe all of those can be improved a bit like
above.

~~~

8.
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+ "INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');

8a.
You can combine multiple of these safe_psql calls together.

NOTE: I won't keep repeating this review comment but I think maybe
there are lots more places where the safe_psql can all be combined to
expected multiple statements.

~

8b.
I felt all those commands should be under the "Test incremental
changes" comment.

~~~

9.
+is($result, qq(1||3), 'check alter publication with EXCEPT');

Maybe that should've said with 'EXCEPT (column-list)'

~~~

10.
+# Test for publication created with publish_generated_columns as true on table
+# with generated columns and column list specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+ "ALTER PUBLICATION tap_pub_col SET (publish_generated_columns)");
+$node_publisher->safe_psql('postgres',
+ "ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');

10a.
I felt the test comments for both those generated columns parameter
test should give more explanation to say what is the expected result
and why.

~

10b.
How does "ALTER PUBLICATION tap_pub_col SET
(publish_generated_columns)" even work? I thought the
"pubish_generated_columns" is an enum but you did not specify any enum
value here (???)

~~~

11.
+ 'check publication(publish_generated_columns as false) with
generated columns and EXCEPT'

Hmm. I thought there is no such thing as "publish_generated_columns as
false", and also the EXCEPT should say 'EXCEPT (column-list)'

~~~

12.
I wonder if there should be another boundary condition test case as follows:
- have some table with cols a,b,c.
- create a publication 'EXCEPT (a,b,c)', so you don't publish anything at all.
- then ALTER the TABLE to add a column 'd'.
- now the publication should publish only 'd'.

======

Kind Regards,
Peter Smith.
Fujitsu Australia

#102shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#96)
Re: Skipping schema changes in publication

On Sat, Jul 19, 2025 at 4:17 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 30 Jun 2025 at 16:25, shveta malik <shveta.malik@gmail.com> wrote:

Few more comments on 002:

5)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
{

+ List    *exceptlist;
+
+ exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);

a) Here, we are assuming that the list provided by
GetPublicationRelations() will be except-tables list only, but there
is no validation of that.
b) We are using GetPublicationRelations() to get the relations which
are excluded from the publication. The name of function and comments
atop function are not in alignment with this usage.

Suggestion:
We can have a new GetPublicationExcludeRelations() function for the
concerned usage. The existing logic of GetPublicationRelations() can
be shifted to a new internal-logic function which will accept a
'except-flag' as well. Both GetPublicationRelations() and
GetPublicationExcludeRelations() can call that new function by passing
'except-flag' as false and true respectively. The new internal
function will validate 'prexcept' against that except-flag passed and
will return the results.

I have made the above change.

Thank You for the changes.

1)
But on rethinking, shall we make GetPublicationRelations() similar to :

/* Gets list of publication oids for a relation that matches the except_flag */
GetRelationPublications(Oid relid, bool except_flag)

i.e. we can have a single function GetPublicationRelations() taking
except_flag and comment can say: 'Gets list of relation oids for a
publication that matches the except_flag.'

We can get rid of GetPubIncludedOrExcludedRels() and
GetPublicationExcludeRelations().

Thoughts?

2)
we can rename except_table to except_flag to be consistent with
GetRelationPublications()

3)
+ if ((except_table && pubrel->prexcept) || !except_table)
+ result = GetPubPartitionOptionRelations(result, pub_partopt,
+ pubrel->prrelid);

3a)
In the case of '!except_table', we are not matching it with
'pubrel->prexcept', is that intentional?

3 b)
Shall we simplify this similar to the changes in GetRelationPublications() i.e.
if (except_table/flag == pubrel->prexcept)
result = GetPubPartitionOptionRelations(...)

6)
Before your patch002, GetTopMostAncestorInPublication() was checking
pg_publication_rel and pg_publication_namespace to find out if the
table in the ancestor-list is part of a given particular. Both
pg_publication_rel and pg_publication_namespace did not have the entry
"for all tables" publications. That means
GetTopMostAncestorInPublication() was originally not checking whether
the given puboid is an "for all tables" publication to see if a rel
belongs to that particular pub or not. I

But now with the current change, we do check if pub is all-tables pub,
if so, return relid and mark ancestor_level (provided table is not
part of the except list). IIUC, the result in 2 cases may be
different. Is that the intention? Let me know if my understanding is
wrong.

This is intentional, in function get_rel_sync_entry, we are setting
pub_relid to the topmost published ancestor. In HEAD we are directly
setting using:
/*
* If this is a FOR ALL TABLES publication, pick the partition
* root and set the ancestor level accordingly.
*/
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
In HEAD, we can directly use 'llast_oid(ancestors)' to get the topmost
ancestor for case of FOR ALL TABLES.
But with this proposal. This change will no longer be valid as the
'llast_oid(ancestors)' may be excluded in the publication. So, to
handle this change was made in GetTopMostAncestorInPublication.

Also, during testing with the partitioned table and
publish_via_partition_root the behaviour of the current patch is as
below:
For example we have a partitioned table t1. It has partitions part1
and part2. Now consider the following cases:
1. with publish_via_partition_root = true
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.

Okay. Agreed.

II. If we create publication on all tables with EXCEPT part1,
data for all tables t1, part1 and part2 is replicated.

Okay. Is this because part1 changes are replicated through t1 and
since t1 changes are not restricted, part1 changes will also not be
restricted? In other words, part1 was never published directly in the
first place and thus 'EXCEPT part1' has no meaning when
'publish_via_partition_root' = true? IMO, it is in alignment with the
'publish_via_partition_root' definition but it might not be that
intuitive for users. So shall we emit a WARNING:

WARNING: Partition "part1" is excluded, but publish_via_partition_root
= true, so this will have no effect.
Thoughts?

2. with publish_via_partition_root = false
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.

I think we shall still publish partitions here. Since
publish_via_partition_root is false, part1 and part2 are published
individually and thus shall we allow publishing of part1 and part 2
here? Thoughts?

II. If we create publication on all tables with EXCEPT part1,
data for part1 is not replicated

Agreed.

thanks
Shveta

#103shveta malik
shveta.malik@gmail.com
In reply to: shveta malik (#102)
Re: Skipping schema changes in publication

Shlok, I was trying to validate the interaction of
'publish_via_partition_root' with 'EXCEPT". Found some unexpected
behaviour, can you please review:

Pub:
---------
CREATE TABLE tab_root (range_col int,i int,j int) PARTITION BY RANGE
(range_col);
CREATE TABLE tab_part_1 PARTITION OF tab_root FOR VALUES FROM (1) to (1000);
CREATE TABLE tab_part_2 PARTITION OF tab_root FOR VALUES FROM (1000) to (2000);
create publication pub2 for all tables except tab_part_2 WITH
(publish_via_partition_root=true);

Sub (tables without partition):
--------
CREATE TABLE tab_root (range_col int,i int,j int);
CREATE TABLE tab_part_1(range_col int,i int,j int);
CREATE TABLE tab_part_2(range_col int,i int,j int);
create subscription sub2 connection '...' publication pub2;

Pub:
--------
insert into tab_part_2 values(1001,1,1);

On Sub, the above row is replicated as expected in tab_root due to
publish_via_partition_root=true on pub.

Now on Pub:
--------
alter publication pub2 set (publish_via_partition_root=false);
insert into tab_part_2 values(1002,2,2);

Now with publish_via_partition_root=false and 'except tab_part_2', the
above row is correctly ignored and not replicated on sub.

But when I try this:
insert into tab_part_1 values(1,1,1);
insert into tab_root values(5,5,5);

Expectation was that the above rows are replicated but that is not the
case. Can you please review? Please let me know if my understanding is
wrong.

thanks
Shveta

#104shveta malik
shveta.malik@gmail.com
In reply to: shveta malik (#103)
Re: Skipping schema changes in publication

I further tested inherited tables flow as well wrt ONLY and EXCEPT, it
works well. But while reading docs for the saem, I have few concerns.

1)
While explaining ONLY for EXCEPT, create-publication doc says this

+      This does not apply to a partitioned table, however.  The partitions of
+      a partitioned table are always implicitly considered part of the
+      publication, so they are never explicitly excluded from the publication.

I do not understand the last line: "so they are never explicitly
excluded from the publication" . But we can explicitly exclude them
using EXCEPT <partition_name>. Do you mean to say something else here?

2)
alter-publication doc says (in context of EXCEPT):

"If ONLY is specified before the table name, only that table is
affected. If ONLY is not specified, the table and all its descendant
tables (if any) are affected. Optionally, * can be specified after
the table name to explicitly indicate that descendant tables are
affected."

But it does not mention anything for partitions. I think we shall
mention here as well that this does not apply to a partitioned table.
(I tested ONLY and EXCEPT for partition-root. UNLIKE inherited tables,
ONLY has no impact on partitioned tables.)

3)
Shall we explain the relation of 'publish_via_partition_root' with
EXCEPT briefly in docs(once we conclude that design)?

Please note that I have performed all the tests (mentioned here and in
previous emails) on patch001 and patch002. patch003 is not applied in
these tests.

thanks
Shveta

#105Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#101)
3 attachment(s)
Re: Skipping schema changes in publication

On Tue, 22 Jul 2025 at 07:28, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

Some review comments for patch v17-0003. I also checked the TAP test this time.

======
doc/src/sgml/logical-replication.sgml

1.
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns in a column list using the <literal>EXCEPT</literal> clause excludes
+   the specified generated columns from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for

I think that is not quite the same wording I had previously suggested.
It sounds a bit odd/redundant saying "Specifying" and "specified" in
the same sentence.

======
src/backend/parser/gram.y

2. check_except_collist

I'm wondering if this checking should be done within the existing
preprocess_pubobj_list() function, alongside all the other ERROR
checking. Care needs to be taken to make sure the pubtable->except is
referring to an EXCEPT (col-list), instead of the other kind of EXCEPT
tables, but in general I think it is better to keep all the
publication combinations checking errors like this in one place.

Added the check in preprocess_pubobj_list(). I checked the syntaxes
and found that this function is not called for "FOR ALL TABLES" cases
and EXCEPT tables can only be used with "FOR ALL TABLES" publications.
So, I think handling for "EXCEPT tables" will not be required in the
function preprocess_pubobj_list()

======
src/bin/psql/describe.c

3. addFooterToPublicationDesc

- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (!PQgetisnull(result, i, 3) &&
+ strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT (%s)",
+   PQgetvalue(result, i, 2));
+ else
+ appendPQExpBuffer(&buf, " (%s)",
+   PQgetvalue(result, i, 2));
+ }

Do you really need to check !PQgetisnull(result, i, 3) here? (e.g.
The comment does not say that this attribute can be NULL)

======
.../t/037_rep_changes_except_collist.pl

4.
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications

Comment is wrong. These tests are for EXCEPT (column-list)

~~~

5.
+# Test for except column publications
+# Initial setup
+$node_publisher->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab3 (a int, b int, c int)");
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab4 (a int, b int GENERATED ALWAYS AS (a * 2) STORED,
c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres',
+ "CREATE TABLE tab5 (a int, b int GENERATED ALWAYS AS (a * 2) STORED,
c int GENERATED ALWAYS AS (a * 3) STORED)"
+);
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+ "INSERT INTO sch1.tab2 VALUES (1, 2, 3)");
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION tap_pub_col FOR TABLE tab2 EXCEPT (a), sch1.tab2
EXCEPT (b, c)"
+);

5a.
I think you don't need to say "Test for except column publications",
because that is the purpose of thie entire file.

~

5b.
You can combine multiple of these safe_psql calls together

~

5c.
It might help make tests easier to read if you named those generated
columns 'b', 'c' cols as 'bgen', 'cgen' instead.

~
5d.
The table names are strange, because why does it start at tab2 when
there is not a tab1?
~~~

6.
+$node_subscriber->safe_psql('postgres', "CREATE SCHEMA sch1");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab2 (a int, b int NOT NULL, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE sch1.tab2 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab3 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab4 (a int, b int, c int)");
+$node_subscriber->safe_psql('postgres',
+ "CREATE TABLE tab5 (a int, b int, c int)");

You can combine multiple of these safe_psql calls together

~~~

7.
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(|2|3),
+ 'check that initial sync for except column publication');

The message seems strange. Do you mean "check initial sync for an
'EXCEPT (column-list)' publication"

NOTE: There are many other messages where you wrote "for except column
publication" but I think maybe all of those can be improved a bit like
above.

~~~

8.
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (4, 5, 6)");
+$node_publisher->safe_psql('postgres',
+ "INSERT INTO sch1.tab2 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');

8a.
You can combine multiple of these safe_psql calls together.

NOTE: I won't keep repeating this review comment but I think maybe
there are lots more places where the safe_psql can all be combined to
expected multiple statements.

~

8b.
I felt all those commands should be under the "Test incremental
changes" comment.

~~~

9.
+is($result, qq(1||3), 'check alter publication with EXCEPT');

Maybe that should've said with 'EXCEPT (column-list)'

~~~

10.
+# Test for publication created with publish_generated_columns as true on table
+# with generated columns and column list specified with EXCEPT
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (1)");
+$node_publisher->safe_psql('postgres',
+ "ALTER PUBLICATION tap_pub_col SET (publish_generated_columns)");
+$node_publisher->safe_psql('postgres',
+ "ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');

10a.
I felt the test comments for both those generated columns parameter
test should give more explanation to say what is the expected result
and why.

~

10b.
How does "ALTER PUBLICATION tap_pub_col SET
(publish_generated_columns)" even work? I thought the
"pubish_generated_columns" is an enum but you did not specify any enum
value here (???)

~~~

Yes, it works. It works equivalent to publish_generated_columns = stored.
Eg:
postgres=# CREATE PUBLICATION pub1 FOR TABLE t1 with
(publish_generated_columns);
CREATE PUBLICATION
postgres=# select * from pg_publication;
oid | pubname | pubowner | puballtables | pubinsert | pubupdate |
pubdelete | pubtruncate | pubviaroot | pubgencols
-------+---------+----------+--------------+-----------+-----------+-----------+-------------+------------+------------
16395 | pub1 | 10 | f | t | t | t
| t | f | s
(1 row)

For this patch, I have modified the test to use
'publish_generated_columns = stored'.

11.
+ 'check publication(publish_generated_columns as false) with
generated columns and EXCEPT'

Hmm. I thought there is no such thing as "publish_generated_columns as
false", and also the EXCEPT should say 'EXCEPT (column-list)'

~~~

12.
I wonder if there should be another boundary condition test case as follows:
- have some table with cols a,b,c.
- create a publication 'EXCEPT (a,b,c)', so you don't publish anything at all.
- then ALTER the TABLE to add a column 'd'.
- now the publication should publish only 'd'.
======

I have fixed all the comments and added the changes in the latest v18 patch.

Thanks,
Shlok Kyal

Attachments:

v18-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v18-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From d7e426c5dc8b79024234c2320c46fddcdc71c6df Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v18 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1bf7eaae5b3..c3af10c4dc6 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1603,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..4a4010296af 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10850,6 +10850,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10896,6 +10898,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1f2ca946fc5..8de7d103846 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,7 +2266,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 1ec3fa34a2d..bcc38f59a97 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1930,6 +1930,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 2585f083181..86709803f00 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1225,6 +1225,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v18-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v18-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From b99067383b781ac9c3625d339ec436332736ffd4 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v18 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 ++++++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++--
 src/backend/catalog/pg_publication.c          |  72 ++++++-
 src/backend/commands/publicationcmds.c        |  30 ++-
 src/backend/parser/gram.y                     |  44 +++--
 src/backend/replication/logical/tablesync.c   |  31 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  61 +++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +++--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 102 +++++++---
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  71 +++++++
 src/test/regress/sql/publication.sql          |  52 +++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 185 ++++++++++++++++++
 19 files changed, 758 insertions(+), 125 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 78803968aba..88aa4a27338 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 8cc0ccb5eee..7e8c9e96c82 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9571cc26931..a137ac15bb3 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1282,6 +1306,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,7 +1331,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
@@ -1315,6 +1339,25 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
 										&(nulls[3]));
+
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
 		}
 		else
 		{
@@ -1322,8 +1365,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1401,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1416,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+				nulls[2] = true;
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b412cd5f016..c7269190fbe 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,7 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ * 	  If any column is missing, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -381,6 +381,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +405,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +496,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1452,6 +1460,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1467,6 +1476,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		exceptDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1483,6 +1493,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull)
+					oldexcept = DatumGetBool(exceptDatum);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1514,7 +1531,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 49bac034d17..4fb087c1b1a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4450,6 +4450,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10759,14 +10764,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10782,7 +10788,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10790,7 +10796,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10800,8 +10806,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10809,25 +10816,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19695,6 +19704,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("EXCEPT clause not allowed for table without column list"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..3925290fc06 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -824,7 +824,8 @@ copy_read_data(void *outbuf, int minread, int maxread)
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *no_cols_published)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -888,7 +889,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +903,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +923,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +970,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*no_cols_published = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1035,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*no_cols_published ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1168,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		no_cols_published = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &no_cols_published);
+
+	if (no_cols_published)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 08111b571de..d186564c297 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT clause in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'no_cols_published' flag is introduced.
+	 */
+	bool		no_cols_published;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,11 +1141,30 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		no_col_published = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				no_col_published = true;
+		}
 
 		/*
 		 * For non-column list publications — e.g. TABLE (without a column
@@ -1134,7 +1172,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		 * of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!no_col_published && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1184,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1194,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->no_cols_published = no_col_published;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->no_cols_published != no_col_published) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1522,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->no_cols_published)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2106,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->no_cols_published = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2156,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->no_cols_published = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6f01105df0d..3b0a1841016 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4841,24 +4841,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4884,10 +4867,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4967,7 +4969,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c178edb4e05..5e2aa1b0cf0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f3fe4ab30f8..8bec60e48e1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,13 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,35 +3039,62 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3104,8 +3132,14 @@ describeOneTableDetails(const char *schemaname,
 
 				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
+				{
+					if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+						appendPQExpBuffer(&buf, " EXCEPT (%s)",
+										  PQgetvalue(result, i, 2));
+					else
+						appendPQExpBuffer(&buf, " (%s)",
+										  PQgetvalue(result, i, 2));
+				}
 
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
@@ -6511,6 +6545,15 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 	if (count > 0)
 		printTableAddFooter(cont, footermsg);
 
+	/*---------------------------------------------------
+	 * Publication description columns:
+	 * [0]: schema name (nspname)
+	 * [1]: table name (relname)
+	 * [2]: row filter expression (prqual), may be NULL
+	 * [3]: column list (comma-separated), may be NULL
+	 * [4]: except flag ("t" if EXCEPT, else "f")
+	 *---------------------------------------------------
+	 */
 	for (i = 0; i < count; i++)
 	{
 		if (as_schema)
@@ -6521,7 +6564,11 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 							  PQgetvalue(res, i, 1));
 
 			if (!PQgetisnull(res, i, 3))
+			{
+				if (strcmp(PQgetvalue(res, i, 4), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
 				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+			}
 
 			if (!PQgetisnull(res, i, 2))
 				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
@@ -6704,6 +6751,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,9 +6765,6 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b3620606d94..ed8f3c8c353 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2272,6 +2272,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3601,7 +3603,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 640ea484c76..78c89bfef5a 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2132,6 +2132,77 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause not allowed for table without column list
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 630631c647f..cc42bfaedaa 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1321,6 +1321,58 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..8c452cfba5d
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,185 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED, bgen int GENERATED ALWAYS AS (2) STORED);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+	CREATE TABLE tab6 (agen int, bgen int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'check that initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'check that initial sync for EXCEPT (column-list) publication');
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 3, b = 4, c = 5 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is( $result, qq(|5|6
+|4|5),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column are is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v18-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v18-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From ecc0082175a870efc28ed062c794e6274d48b9db Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v18 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 215 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 858 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 97f547b3cc4..78803968aba 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index fcac55aefe6..8cc0ccb5eee 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2283,10 +2283,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 4f7b11175c6..cb4215071d0 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..9571cc26931 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index c3af10c4dc6..b412cd5f016 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -525,7 +532,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1028,7 +1033,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1148,7 +1153,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1271,7 +1297,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1289,6 +1318,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1327,7 +1429,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1428,6 +1531,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1478,7 +1582,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1601,6 +1706,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1813,6 +1932,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1885,6 +2005,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1960,8 +2081,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..47916ef32ae 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a4010296af..49bac034d17 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10702,7 +10703,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10722,12 +10723,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10765,6 +10767,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10840,6 +10843,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10852,6 +10874,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10878,6 +10902,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index f4c977262c5..08111b571de 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..5d55f1f4ece 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f3a353a61a5..6f01105df0d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4573,8 +4575,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4740,6 +4768,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4751,8 +4780,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4763,6 +4800,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4778,6 +4816,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4789,6 +4828,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4802,7 +4842,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4843,6 +4887,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11705,6 +11752,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20071,6 +20121,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..c178edb4e05 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index a02da3e9652..40fdfcb121c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -429,6 +431,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1701,6 +1714,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index a86b38466de..ba9f0f68e00 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3323,6 +3323,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7a06af48842..f3fe4ab30f8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8de7d103846..b3620606d94 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,11 +2269,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3588,6 +3593,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index bcc38f59a97..640ea484c76 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -210,13 +210,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -235,8 +259,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1933,9 +1974,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1952,7 +1999,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1971,6 +2035,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1989,6 +2058,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2006,6 +2081,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2044,9 +2125,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 86709803f00..630631c647f 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -104,20 +104,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1228,17 +1241,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1246,6 +1272,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1253,6 +1282,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1260,6 +1293,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1276,10 +1313,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#106Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#102)
Re: Skipping schema changes in publication

On Tue, 22 Jul 2025 at 14:29, shveta malik <shveta.malik@gmail.com> wrote:

On Sat, Jul 19, 2025 at 4:17 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 30 Jun 2025 at 16:25, shveta malik <shveta.malik@gmail.com> wrote:

Few more comments on 002:

5)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
{

+ List    *exceptlist;
+
+ exceptlist = GetPublicationRelations(pubid, PUBLICATION_PART_ALL);

a) Here, we are assuming that the list provided by
GetPublicationRelations() will be except-tables list only, but there
is no validation of that.
b) We are using GetPublicationRelations() to get the relations which
are excluded from the publication. The name of function and comments
atop function are not in alignment with this usage.

Suggestion:
We can have a new GetPublicationExcludeRelations() function for the
concerned usage. The existing logic of GetPublicationRelations() can
be shifted to a new internal-logic function which will accept a
'except-flag' as well. Both GetPublicationRelations() and
GetPublicationExcludeRelations() can call that new function by passing
'except-flag' as false and true respectively. The new internal
function will validate 'prexcept' against that except-flag passed and
will return the results.

I have made the above change.

Thank You for the changes.

1)
But on rethinking, shall we make GetPublicationRelations() similar to :

/* Gets list of publication oids for a relation that matches the except_flag */
GetRelationPublications(Oid relid, bool except_flag)

i.e. we can have a single function GetPublicationRelations() taking
except_flag and comment can say: 'Gets list of relation oids for a
publication that matches the except_flag.'

We can get rid of GetPubIncludedOrExcludedRels() and
GetPublicationExcludeRelations().

Thoughts?

This seems reasonable to me. I have made the changes for the same.

2)
we can rename except_table to except_flag to be consistent with
GetRelationPublications()

3)
+ if ((except_table && pubrel->prexcept) || !except_table)
+ result = GetPubPartitionOptionRelations(result, pub_partopt,
+ pubrel->prrelid);

3a)
In the case of '!except_table', we are not matching it with
'pubrel->prexcept', is that intentional?

3 b)
Shall we simplify this similar to the changes in GetRelationPublications() i.e.
if (except_table/flag == pubrel->prexcept)
result = GetPubPartitionOptionRelations(...)

6)
Before your patch002, GetTopMostAncestorInPublication() was checking
pg_publication_rel and pg_publication_namespace to find out if the
table in the ancestor-list is part of a given particular. Both
pg_publication_rel and pg_publication_namespace did not have the entry
"for all tables" publications. That means
GetTopMostAncestorInPublication() was originally not checking whether
the given puboid is an "for all tables" publication to see if a rel
belongs to that particular pub or not. I

But now with the current change, we do check if pub is all-tables pub,
if so, return relid and mark ancestor_level (provided table is not
part of the except list). IIUC, the result in 2 cases may be
different. Is that the intention? Let me know if my understanding is
wrong.

This is intentional, in function get_rel_sync_entry, we are setting
pub_relid to the topmost published ancestor. In HEAD we are directly
setting using:
/*
* If this is a FOR ALL TABLES publication, pick the partition
* root and set the ancestor level accordingly.
*/
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
In HEAD, we can directly use 'llast_oid(ancestors)' to get the topmost
ancestor for case of FOR ALL TABLES.
But with this proposal. This change will no longer be valid as the
'llast_oid(ancestors)' may be excluded in the publication. So, to
handle this change was made in GetTopMostAncestorInPublication.

Also, during testing with the partitioned table and
publish_via_partition_root the behaviour of the current patch is as
below:
For example we have a partitioned table t1. It has partitions part1
and part2. Now consider the following cases:
1. with publish_via_partition_root = true
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.

Okay. Agreed.

II. If we create publication on all tables with EXCEPT part1,
data for all tables t1, part1 and part2 is replicated.

Okay. Is this because part1 changes are replicated through t1 and
since t1 changes are not restricted, part1 changes will also not be
restricted? In other words, part1 was never published directly in the
first place and thus 'EXCEPT part1' has no meaning when
'publish_via_partition_root' = true? IMO, it is in alignment with the
'publish_via_partition_root' definition but it might not be that
intuitive for users. So shall we emit a WARNING:

WARNING: Partition "part1" is excluded, but publish_via_partition_root
= true, so this will have no effect.
Thoughts?

Your understanding is correct. I have added a WARNING for this case

2. with publish_via_partition_root = false
I. If we create publication on all tables with EXCEPT t1, no data
for t1, part1 or part2 is replicated.

I think we shall still publish partitions here. Since
publish_via_partition_root is false, part1 and part2 are published
individually and thus shall we allow publishing of part1 and part 2
here? Thoughts?

I made a mistake in explaining this point. Yes your point is correct.
Changes for partitions part1 and part2 will be replicated.
I have documented the behaviour in the docs.

II. If we create publication on all tables with EXCEPT part1,
data for part1 is not replicated

Agreed.

I have addressed the comments and have attached the updated patch in [1]/messages/by-id/CANhcyEXkeg3sjkS3DS9yU1ckz4ozUBNZ+RmrWaRNSSVCR8RquA@mail.gmail.com.
[1]: /messages/by-id/CANhcyEXkeg3sjkS3DS9yU1ckz4ozUBNZ+RmrWaRNSSVCR8RquA@mail.gmail.com

Thanks,
Shlok Kyal

#107Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#103)
Re: Skipping schema changes in publication

On Tue, 22 Jul 2025 at 15:57, shveta malik <shveta.malik@gmail.com> wrote:

Shlok, I was trying to validate the interaction of
'publish_via_partition_root' with 'EXCEPT". Found some unexpected
behaviour, can you please review:

Pub:
---------
CREATE TABLE tab_root (range_col int,i int,j int) PARTITION BY RANGE
(range_col);
CREATE TABLE tab_part_1 PARTITION OF tab_root FOR VALUES FROM (1) to (1000);
CREATE TABLE tab_part_2 PARTITION OF tab_root FOR VALUES FROM (1000) to (2000);
create publication pub2 for all tables except tab_part_2 WITH
(publish_via_partition_root=true);

Sub (tables without partition):
--------
CREATE TABLE tab_root (range_col int,i int,j int);
CREATE TABLE tab_part_1(range_col int,i int,j int);
CREATE TABLE tab_part_2(range_col int,i int,j int);
create subscription sub2 connection '...' publication pub2;

Pub:
--------
insert into tab_part_2 values(1001,1,1);

On Sub, the above row is replicated as expected in tab_root due to
publish_via_partition_root=true on pub.

Now on Pub:
--------
alter publication pub2 set (publish_via_partition_root=false);
insert into tab_part_2 values(1002,2,2);

Now with publish_via_partition_root=false and 'except tab_part_2', the
above row is correctly ignored and not replicated on sub.

But when I try this:
insert into tab_part_1 values(1,1,1);
insert into tab_root values(5,5,5);

Expectation was that the above rows are replicated but that is not the
case. Can you please review? Please let me know if my understanding is
wrong.

Hi Shveta,

I checked this behaviour on HEAD and found that it is the same
behaviour as HEAD. I think if we alter the parameter
'publish_via_partition_root', we should do ALTER SUBSCRIPTION ..
REFRESH PUBLICATION on subscriber.
I reviewed your behaviour and saw that after the 'alter publication
pub2 set (publish_via_partition_root=false)', the changes are still
being replicated to 'tab_root' on subscriber. And this behaviour is
similar to HEAD.

For example:
Pub:
---------
CREATE TABLE tab_root (range_col int,i int,j int) PARTITION BY RANGE
(range_col);
CREATE TABLE tab_part_1 PARTITION OF tab_root FOR VALUES FROM (1) to (1000);
CREATE TABLE tab_part_2 PARTITION OF tab_root FOR VALUES FROM (1000) to (2000);
create publication pub2 for table tab_root WITH
(publish_via_partition_root=true);

Sub (tables without partition):
--------
CREATE TABLE tab_root (range_col int,i int,j int);
CREATE TABLE tab_part_1(range_col int,i int,j int);
CREATE TABLE tab_part_2(range_col int,i int,j int);
create subscription sub2 connection '...' publication pub2;

Pub:
--------
insert into tab_part_2 values(1001,1,1);

On Sub, the above row is replicated as expected in tab_root.

Now on Pub:
--------
alter publication pub2 set (publish_via_partition_root=false);

when I try this the data:
insert into tab_part_2 values(1002,2,2);
insert into tab_part_1 values(1,1,1);
insert into tab_root values(5,5,5);

The data is being replicated to tab_root on the subscriber.

After I do ALTER SUBSCRIPTION .. REFRESH PUBLICATION on subscriber,
replication happens as expected.

Also I found following documentation:
"Altering the <literal>publish_via_partition_root</literal> parameter can
lead to data loss or duplication at the subscriber because it changes
the identity and schema of the published tables. Note this happens only
when a partition root table is specified as the replication target."

Thanks,
Shlok Kyal

#108Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#104)
Re: Skipping schema changes in publication

On Wed, 23 Jul 2025 at 10:08, shveta malik <shveta.malik@gmail.com> wrote:

I further tested inherited tables flow as well wrt ONLY and EXCEPT, it
works well. But while reading docs for the saem, I have few concerns.

1)
While explaining ONLY for EXCEPT, create-publication doc says this

+      This does not apply to a partitioned table, however.  The partitions of
+      a partitioned table are always implicitly considered part of the
+      publication, so they are never explicitly excluded from the publication.

I do not understand the last line: "so they are never explicitly
excluded from the publication" . But we can explicitly exclude them
using EXCEPT <partition_name>. Do you mean to say something else here?

2)
alter-publication doc says (in context of EXCEPT):

"If ONLY is specified before the table name, only that table is
affected. If ONLY is not specified, the table and all its descendant
tables (if any) are affected. Optionally, * can be specified after
the table name to explicitly indicate that descendant tables are
affected."

But it does not mention anything for partitions. I think we shall
mention here as well that this does not apply to a partitioned table.
(I tested ONLY and EXCEPT for partition-root. UNLIKE inherited tables,
ONLY has no impact on partitioned tables.)

3)
Shall we explain the relation of 'publish_via_partition_root' with
EXCEPT briefly in docs(once we conclude that design)?

Please note that I have performed all the tests (mentioned here and in
previous emails) on patch001 and patch002. patch003 is not applied in
these tests.

I have added/ modified the documentations as per the comments. The
changes are present in patch [1]/messages/by-id/CANhcyEXkeg3sjkS3DS9yU1ckz4ozUBNZ+RmrWaRNSSVCR8RquA@mail.gmail.com.
[1]: /messages/by-id/CANhcyEXkeg3sjkS3DS9yU1ckz4ozUBNZ+RmrWaRNSSVCR8RquA@mail.gmail.com

Thanks,
Shlok Kyal

#109Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#105)
Re: Skipping schema changes in publication

On Mon, Aug 4, 2025 at 2:07 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

...

10b.
How does "ALTER PUBLICATION tap_pub_col SET
(publish_generated_columns)" even work? I thought the
"pubish_generated_columns" is an enum but you did not specify any enum
value here (???)

~~~

Yes, it works. It works equivalent to publish_generated_columns = stored.
Eg:
postgres=# CREATE PUBLICATION pub1 FOR TABLE t1 with
(publish_generated_columns);
CREATE PUBLICATION
postgres=# select * from pg_publication;
oid | pubname | pubowner | puballtables | pubinsert | pubupdate |
pubdelete | pubtruncate | pubviaroot | pubgencols
-------+---------+----------+--------------+-----------+-----------+-----------+-------------+------------+------------
16395 | pub1 | 10 | f | t | t | t
| t | f | s
(1 row)

Hmm -- it's not documented to behave like that, so I've created
another thread for getting to the bottom of this topic.

~~~

Meanwhile, here are my review comments for patch v18-0003

======
src/backend/catalog/pg_publication.c

pg_get_publication_tables:

1.
if (nattnums > 0)
{
values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
nulls[2] = false;
}
else
nulls[2] = true;

Is there any possibility that values[2] might not be null, but then
nattrnums skips some cols so remains 0? Then the final values[2] would
conflict with nulls[2], which seems strange. Maybe it is safer to also
assign values[2] = null in the else.

======
src/backend/replication/logical/tablesync.c

fetch_remote_table_info:

2.
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
- List **qual, bool *gencol_published)
+ List **qual, bool *gencol_published,
+ bool *no_cols_published)

This new parameter should be documented in the function comment.

~~~

3.
+ if (server_version >= 190000)
+ *no_cols_published = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+

It seems that *no_cols_published (and *gencol_published) are assigned
false by the caller. I had to go looking for that, so IMO it would be
better to put Assert at the top of here so it is self-documenting

Assert(*gencol_published == false);
Assert(*no_cols_published == false);

======
src/backend/replication/pgoutput/pgoutput.c

4.
+ /*
+ * Indicates whether no columns are published for a given relation. With
+ * the introduction of the EXCEPT clause in column lists, it is now
+ * possible to define a publication that excludes all columns of a table.
+ * However, the 'columns' attribute cannot represent this case, since a
+ * NULL value implies that all columns are published. To distinguish this
+ * scenario, the 'no_cols_published' flag is introduced.
+ */
+ bool no_cols_published;

The wording of the comment seems a bit strange -- EXCEPT is not a clause.

BEFORE:
the introduction of the EXCEPT clause in column lists, ...

SUGGESTION
the introduction of the EXCEPT qualifier for column lists, ....

~~~

5.
  Bitmapset  *cols = NULL;
+ bool except_columns = false;
+ bool no_col_published = false;

There are multiple places in this patch that say:

'no_col_published'
or 'no_cols_published'

I felt this var name can be misunderstood because it is easy to read
"no" as meaning "no." (aka number), and then misinterpret as
"number_of_cols_published".

Maybe an unambiguous name can be found, like
- 'zero_cols_published' or
- 'nothing_published' or
- really make it 'num_cols_published' and check for 0.

(so this comment applies to multiple places in the patch)

~~

6.
* of the table (including generated columns when
* 'publish_generated_columns' parameter is true).
*/
- if (!cols)
+ if (!no_col_published && !cols)
{

The existing comment above this code fragment also needs to mention
"EXCEPT (column-list)" where all the columns are excluded

======
src/bin/psql/describe.c

describeOneTableDetails:

7.
  /* column list (if any) */
  if (!PQgetisnull(result, i, 2))
- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT (%s)",
+   PQgetvalue(result, i, 2));
+ else
+ appendPQExpBuffer(&buf, " (%s)",
+   PQgetvalue(result, i, 2));
+ }

Isn't this code fragment (and also surrounding code) using the same
logic as what is already encapsulated in the function
addFooterToPublicationDesc()?
Superficially, it seems like a large chunk can all be replaced with a
single call to the existing function.

======
src/test/regress/expected/publication.out

8.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause not allowed for table without column list
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^

Is that a bad syntax position marker (^)? e.g. Why is it pointed at
the word "TABLE" instead of "EXCEPT"?

======
.../t/037_rep_changes_except_collist.pl

9.
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+ 'check that initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+ 'check that initial sync for EXCEPT (column-list) publication');

These messages still seem to have missing or extra words: "check that
initial sync" (??). Maybe just remove the word 'that'?

~~~

10.
# Test for update
$node_subscriber->safe_psql(
'postgres', qq(
CREATE UNIQUE INDEX b_idx ON tab1 (b);
ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
));
$node_publisher->safe_psql(
'postgres', qq(
CREATE UNIQUE INDEX b_idx ON tab1 (b);
ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
UPDATE tab1 SET a = 3, b = 4, c = 5 WHERE a = 1;
));
$node_publisher->wait_for_catchup('tap_sub_col');
$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
is( $result, qq(|5|6
|4|5),
'check update for EXCEPT (column-list) publication');

~

10a.
I think the test is OK, but your chosen numbers like 1,2,3, then 4,5,6
and then updating to 1,2,3 to 3,4,5 make it quite hard to review.
Maybe use easier numbers that are more identifiable, e.g. update 1,2,3
=> 991,992,993 or something like that.

~

10b.
You may need to put some ORDER BY in all these queries just to make
sure they are always reproducible, giving rows in the expected order.

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

#110Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#109)
3 attachment(s)
Re: Skipping schema changes in publication

On Mon, 4 Aug 2025 at 13:03, Peter Smith <smithpb2250@gmail.com> wrote:

On Mon, Aug 4, 2025 at 2:07 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

...

10b.
How does "ALTER PUBLICATION tap_pub_col SET
(publish_generated_columns)" even work? I thought the
"pubish_generated_columns" is an enum but you did not specify any enum
value here (???)

~~~

Yes, it works. It works equivalent to publish_generated_columns = stored.
Eg:
postgres=# CREATE PUBLICATION pub1 FOR TABLE t1 with
(publish_generated_columns);
CREATE PUBLICATION
postgres=# select * from pg_publication;
oid | pubname | pubowner | puballtables | pubinsert | pubupdate |
pubdelete | pubtruncate | pubviaroot | pubgencols
-------+---------+----------+--------------+-----------+-----------+-----------+-------------+------------+------------
16395 | pub1 | 10 | f | t | t | t
| t | f | s
(1 row)

Hmm -- it's not documented to behave like that, so I've created
another thread for getting to the bottom of this topic.

~~~

Meanwhile, here are my review comments for patch v18-0003

======
src/backend/catalog/pg_publication.c

pg_get_publication_tables:

1.
if (nattnums > 0)
{
values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
nulls[2] = false;
}
else
nulls[2] = true;

Is there any possibility that values[2] might not be null, but then
nattrnums skips some cols so remains 0? Then the final values[2] would
conflict with nulls[2], which seems strange. Maybe it is safer to also
assign values[2] = null in the else.

Yes, When all the columns of a table are present in 'EXCEPT
(column-list)'. Then effectively no column should be replicated. In
such cases we should mark nulls[2] as true.
I agree with your point that values[2] should be made null. I have
used '(Datum) 0', in accordance with other places.

======
src/backend/replication/logical/tablesync.c

fetch_remote_table_info:

2.
static void
fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
- List **qual, bool *gencol_published)
+ List **qual, bool *gencol_published,
+ bool *no_cols_published)

This new parameter should be documented in the function comment.

~~~

3.
+ if (server_version >= 190000)
+ *no_cols_published = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+

It seems that *no_cols_published (and *gencol_published) are assigned
false by the caller. I had to go looking for that, so IMO it would be
better to put Assert at the top of here so it is self-documenting

Assert(*gencol_published == false);
Assert(*no_cols_published == false);

======
src/backend/replication/pgoutput/pgoutput.c

4.
+ /*
+ * Indicates whether no columns are published for a given relation. With
+ * the introduction of the EXCEPT clause in column lists, it is now
+ * possible to define a publication that excludes all columns of a table.
+ * However, the 'columns' attribute cannot represent this case, since a
+ * NULL value implies that all columns are published. To distinguish this
+ * scenario, the 'no_cols_published' flag is introduced.
+ */
+ bool no_cols_published;

The wording of the comment seems a bit strange -- EXCEPT is not a clause.

BEFORE:
the introduction of the EXCEPT clause in column lists, ...

SUGGESTION
the introduction of the EXCEPT qualifier for column lists, ....

~~~

5.
Bitmapset  *cols = NULL;
+ bool except_columns = false;
+ bool no_col_published = false;

There are multiple places in this patch that say:

'no_col_published'
or 'no_cols_published'

I felt this var name can be misunderstood because it is easy to read
"no" as meaning "no." (aka number), and then misinterpret as
"number_of_cols_published".

Maybe an unambiguous name can be found, like
- 'zero_cols_published' or
- 'nothing_published' or
- really make it 'num_cols_published' and check for 0.

(so this comment applies to multiple places in the patch)

How about 'all_cols_excluded'? Or 'has_published_cols'?
I have used 'all_cols_excluded' in this patch. Thoughts?

~~

6.
* of the table (including generated columns when
* 'publish_generated_columns' parameter is true).
*/
- if (!cols)
+ if (!no_col_published && !cols)
{

The existing comment above this code fragment also needs to mention
"EXCEPT (column-list)" where all the columns are excluded

======
src/bin/psql/describe.c

describeOneTableDetails:

7.
/* column list (if any) */
if (!PQgetisnull(result, i, 2))
- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT (%s)",
+   PQgetvalue(result, i, 2));
+ else
+ appendPQExpBuffer(&buf, " (%s)",
+   PQgetvalue(result, i, 2));
+ }

Isn't this code fragment (and also surrounding code) using the same
logic as what is already encapsulated in the function
addFooterToPublicationDesc()?
Superficially, it seems like a large chunk can all be replaced with a
single call to the existing function.

'addFooterToPublicationDesc' is called when we use \dRp+ and print in format:
"schema_name.table_name" EXCEPT (column-list)
Whereas code pasted above is executed when we use \d+ table_name and
the output is the format:
"publication_name" EXCEPT (column-list)

These pieces of code are used to print different info. One is used to
print info related to tables and the other is used to print info
related to publication.
Should we use a common function for this?

======
src/test/regress/expected/publication.out

8.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause not allowed for table without column list
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^

Is that a bad syntax position marker (^)? e.g. Why is it pointed at
the word "TABLE" instead of "EXCEPT"?

In function 'preprocess_pubobj_list' the position of position marker
(^) is decided by "pubobj->location". Function handles multiple errors
and setting "$$->location" only specific to EXCEPT qualifier would not
be appropriate. One solution I feel is to not show "position marker
(^)" in the case of EXCEPT. Or maybe we can add a new variable to
'PublicationTable' for except_location but I think we should not do
that. Thoughts?

For this version of patch, I have removed the "position marker (^)" in
the case of EXCEPT.

======
.../t/037_rep_changes_except_collist.pl

9.
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+ 'check that initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+ 'check that initial sync for EXCEPT (column-list) publication');

These messages still seem to have missing or extra words: "check that
initial sync" (??). Maybe just remove the word 'that'?

~~~

10.
# Test for update
$node_subscriber->safe_psql(
'postgres', qq(
CREATE UNIQUE INDEX b_idx ON tab1 (b);
ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
));
$node_publisher->safe_psql(
'postgres', qq(
CREATE UNIQUE INDEX b_idx ON tab1 (b);
ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
UPDATE tab1 SET a = 3, b = 4, c = 5 WHERE a = 1;
));
$node_publisher->wait_for_catchup('tap_sub_col');
$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
is( $result, qq(|5|6
|4|5),
'check update for EXCEPT (column-list) publication');

~

10a.
I think the test is OK, but your chosen numbers like 1,2,3, then 4,5,6
and then updating to 1,2,3 to 3,4,5 make it quite hard to review.
Maybe use easier numbers that are more identifiable, e.g. update 1,2,3
=> 991,992,993 or something like that.

~

10b.
You may need to put some ORDER BY in all these queries just to make
sure they are always reproducible, giving rows in the expected order.

I have also addressed the remaining comments and attached the latest
v19 patches.

Thanks,
Shlok Kyal

Attachments:

v19-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v19-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 73721a137c421596eb748039af3a9a3cbb646571 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v19 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 803c26ab216..06f6f45526b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1603,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..4a4010296af 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10850,6 +10850,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10896,6 +10898,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1f2ca946fc5..8de7d103846 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,7 +2266,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 53268059142..74009a92f3f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index deddf0da844..1366b11bba0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v19-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v19-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 4e329e121504f400faca2e00963d91135962bad0 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v19 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 ++++++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++--
 src/backend/catalog/pg_publication.c          |  75 ++++++-
 src/backend/commands/publicationcmds.c        |  30 ++-
 src/backend/parser/gram.y                     |  43 ++--
 src/backend/replication/logical/tablesync.c   |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +++--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       |  99 ++++++---
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  69 +++++++
 src/test/regress/sql/publication.sql          |  52 +++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 190 ++++++++++++++++++
 19 files changed, 776 insertions(+), 128 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e96a55fecf9..c14077caa68 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 414a314acc5..2f04f93620e 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9571cc26931..835694b5908 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1282,6 +1306,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,7 +1331,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
@@ -1315,6 +1339,25 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
 										&(nulls[3]));
+
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
 		}
 		else
 		{
@@ -1322,8 +1365,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1401,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1416,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b6d546be291..0fd83d07275 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,7 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ * 	  If any column is missing, *invalid_column_list is set
  *    to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
@@ -381,6 +381,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +405,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +496,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1452,6 +1460,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1467,6 +1476,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				bool		isnull = true;
 				Datum		whereClauseDatum;
 				Datum		columnListDatum;
+				Datum		exceptDatum;
 
 				/* Load the WHERE clause for this table. */
 				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
@@ -1483,6 +1493,13 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (!isnull)
 					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
 
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull)
+					oldexcept = DatumGetBool(exceptDatum);
+
 				ReleaseSysCache(rftuple);
 			}
 
@@ -1514,7 +1531,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 49bac034d17..927129eeb10 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4450,6 +4450,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10759,14 +10764,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10782,7 +10788,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10790,7 +10796,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10800,8 +10806,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10809,25 +10816,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19695,6 +19704,12 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("EXCEPT clause not allowed for table without column list"));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..68ff559e80c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -821,10 +821,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -838,6 +846,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -888,7 +899,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +913,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +933,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +980,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1045,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1178,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b9d676d1f18..1cf90f1875d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,19 +1141,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1187,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1197,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2109,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2159,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6f01105df0d..3b0a1841016 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4841,24 +4841,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4884,10 +4867,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4967,7 +4969,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c178edb4e05..5e2aa1b0cf0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f3fe4ab30f8..6b375e9772f 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3019,12 +3019,13 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,35 +3039,62 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3104,8 +3132,11 @@ describeOneTableDetails(const char *schemaname,
 
 				/* column list (if any) */
 				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
+				{
+					if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+						appendPQExpBuffer(&buf, " EXCEPT");
+					appendPQExpBuffer(&buf, " (%s)", PQgetvalue(result, i, 2));
+				}
 
 				/* row filter (if any) */
 				if (!PQgetisnull(result, i, 1))
@@ -6511,6 +6542,15 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 	if (count > 0)
 		printTableAddFooter(cont, footermsg);
 
+	/*---------------------------------------------------
+	 * Publication description columns:
+	 * [0]: schema name (nspname)
+	 * [1]: table name (relname)
+	 * [2]: row filter expression (prqual), may be NULL
+	 * [3]: column list (comma-separated), may be NULL
+	 * [4]: except flag ("t" if EXCEPT, else "f")
+	 *---------------------------------------------------
+	 */
 	for (i = 0; i < count; i++)
 	{
 		if (as_schema)
@@ -6521,7 +6561,11 @@ addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
 							  PQgetvalue(res, i, 1));
 
 			if (!PQgetisnull(res, i, 3))
+			{
+				if (strcmp(PQgetvalue(res, i, 4), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
 				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
+			}
 
 			if (!PQgetisnull(res, i, 2))
 				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
@@ -6704,6 +6748,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,9 +6762,6 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b3620606d94..ed8f3c8c353 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2272,6 +2272,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3601,7 +3603,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 49afeb77622..ed8a7b9a3a7 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,75 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause not allowed for table without column list
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6240cd97ce3..c599772a441 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,58 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..79e63c0f449
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,190 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED, bgen int GENERATED ALWAYS AS (2) STORED);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+	CREATE TABLE tab6 (agen int, bgen int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'check initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'check initial sync for EXCEPT (column-list) publication');
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column are is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v19-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v19-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 51841199427801dadf792a7a98d915d6eb35202e Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v19 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 215 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 858 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da8a7882580..e96a55fecf9 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index a0761cfee3f..414a314acc5 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2299,10 +2299,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 4f7b11175c6..cb4215071d0 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..9571cc26931 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 06f6f45526b..b6d546be291 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -525,7 +532,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1028,7 +1033,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1148,7 +1153,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1271,7 +1297,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1289,6 +1318,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1327,7 +1429,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1428,6 +1531,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1478,7 +1582,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1601,6 +1706,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1813,6 +1932,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1885,6 +2005,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1960,8 +2081,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..47916ef32ae 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a4010296af..49bac034d17 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10702,7 +10703,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10722,12 +10723,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10765,6 +10767,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10840,6 +10843,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10852,6 +10874,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10878,6 +10902,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 80540c017bd..b9d676d1f18 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2c..5d55f1f4ece 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f3a353a61a5..6f01105df0d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4573,8 +4575,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4740,6 +4768,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4751,8 +4780,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4763,6 +4800,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4778,6 +4816,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4789,6 +4828,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4802,7 +4842,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4843,6 +4887,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11705,6 +11752,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20071,6 +20121,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..c178edb4e05 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index a02da3e9652..40fdfcb121c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -429,6 +431,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1701,6 +1714,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index a86b38466de..ba9f0f68e00 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3323,6 +3323,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7a06af48842..f3fe4ab30f8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8de7d103846..b3620606d94 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,11 +2269,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3588,6 +3593,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 74009a92f3f..49afeb77622 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1964,6 +2028,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1982,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1999,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2037,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 1366b11bba0..6240cd97ce3 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,17 +1238,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1243,6 +1269,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#111Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#110)
Re: Skipping schema changes in publication

Hi Shlok.

On Wed, Aug 6, 2025 at 11:11 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

...

5.
Bitmapset  *cols = NULL;
+ bool except_columns = false;
+ bool no_col_published = false;

There are multiple places in this patch that say:

'no_col_published'
or 'no_cols_published'

I felt this var name can be misunderstood because it is easy to read
"no" as meaning "no." (aka number), and then misinterpret as
"number_of_cols_published".

Maybe an unambiguous name can be found, like
- 'zero_cols_published' or
- 'nothing_published' or
- really make it 'num_cols_published' and check for 0.

(so this comment applies to multiple places in the patch)

How about 'all_cols_excluded'? Or 'has_published_cols'?
I have used 'all_cols_excluded' in this patch. Thoughts?

The new name is good.

======
src/bin/psql/describe.c

describeOneTableDetails:

7.
/* column list (if any) */
if (!PQgetisnull(result, i, 2))
- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT (%s)",
+   PQgetvalue(result, i, 2));
+ else
+ appendPQExpBuffer(&buf, " (%s)",
+   PQgetvalue(result, i, 2));
+ }

Isn't this code fragment (and also surrounding code) using the same
logic as what is already encapsulated in the function
addFooterToPublicationDesc()?
Superficially, it seems like a large chunk can all be replaced with a
single call to the existing function.

'addFooterToPublicationDesc' is called when we use \dRp+ and print in format:
"schema_name.table_name" EXCEPT (column-list)
Whereas code pasted above is executed when we use \d+ table_name and
the output is the format:
"publication_name" EXCEPT (column-list)

These pieces of code are used to print different info. One is used to
print info related to tables and the other is used to print info
related to publication.
Should we use a common function for this?

It still seems like quite a lot of overlap. e.g. I thought there were
~30 lines common. OTOH, perhaps you'll need to pass another boolean to
the function to indicate it is a "Publication:" footer. I guess you'd
have to try it out first to see if the changes required to save those
30 LOC are worthwhile or not.

======
src/test/regress/expected/publication.out

8.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause not allowed for table without column list
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^

Is that a bad syntax position marker (^)? e.g. Why is it pointed at
the word "TABLE" instead of "EXCEPT"?

In function 'preprocess_pubobj_list' the position of position marker
(^) is decided by "pubobj->location". Function handles multiple errors
and setting "$$->location" only specific to EXCEPT qualifier would not
be appropriate. One solution I feel is to not show "position marker
(^)" in the case of EXCEPT. Or maybe we can add a new variable to
'PublicationTable' for except_location but I think we should not do
that. Thoughts?

In the review comments below, I suggest putting this location back,
but changing the message.

For this version of patch, I have removed the "position marker (^)" in
the case of EXCEPT.

//////

Here are my review comments for the patch v19-0003.

======
1. General - SGML tags in docs for table/column names.

There is nothing to change just yet, but keep an eye on the thread
[1]: /messages/by-id/aIELRMAviNiUL1ie@momjian.us
in this patch for table/column names that will need to be updated for
consistency.

======
src/backend/catalog/pg_publication.c

pg_get_publication_tables:

2.
+
+ if (!nulls[2])
+ {
+ Datum exceptDatum;
+ bool isnull;
+
+ /*
+ * We fetch pubtuple if publication is not FOR ALL TABLES and
+ * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+ * indicates that prattrs contains columns to be excluded for
+ * replication.
+ */
+ exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+   Anum_pg_publication_rel_prexcept,
+   &isnull);
+
+ if (!isnull && DatumGetBool(exceptDatum))
+ except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+ }

Maybe this should be done a few lines earlier, to keep all the
values[2]/nulls[2] code together, ahead of the values[3]/nulls[3]
code. Indeed, there is lots of other values[2]/nulls[2] logic that
comes later in this function, so maybe it is better to do all of that
first, instead of mingling it with values[3]/nulls[3].

======
src/backend/commands/publicationcmds.c

pub_contains_invalid_column:

3.
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ *   If any column is missing, *invalid_column_list is set
  *    to true.

Whitespace problem here; there is some tab instead of space in this comment.

Also /part of column list/part of the column list/

~~~

AlterPublicationTables:

4.
bool isnull = true;
Datum whereClauseDatum;
Datum columnListDatum;
+ Datum exceptDatum;

It's not necessary to have all these different Datum variables; they
are only temporary storage. It might be simpler to use a single "Datum
datum;" which is reused 3x.

~

5.
+ exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+   Anum_pg_publication_rel_prexcept,
+   &isnull);
+
+ if (!isnull)
+ oldexcept = DatumGetBool(exceptDatum);
+

Isn't the 'prexcept' also used for EXCEPT TABLE as well as EXCEPT
(column-list)? In other words, should the change to this function be
done already in one of the earlier patches?

~

6.
  if (equal(oldrelwhereclause, newpubrel->whereClause) &&
- bms_equal(oldcolumns, newcolumns))
+ bms_equal(oldcolumns, newcolumns) &&
+ oldexcept == newpubrel->except)

The code comment about this code fragment should also mention EXCEPT.

======
src/backend/parser/gram.y

preprocess_pubobj_list:

7.
+ if (pubobj->pubtable && pubobj->pubtable->except &&
+ pubobj->pubtable->columns == NULL)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("EXCEPT clause not allowed for table without column list"));
+

Having the syntax error location (like before in v18) might be better,
but since that location is associated with the TABLE, then the error
message should also be reworded so the subject is the table.

SUGGESTION
errmsg("table without column list cannot use EXCEPT clause")

======
src/bin/psql/describe.c

describeOneTableDetails:

8.
- if (pset.sversion >= 150000)
+ if (pset.sversion >= 190000)
  {
  printfPQExpBuffer(&buf,
    "SELECT pubname\n"
    "     , NULL\n"
    "     , NULL\n"
+   " , NULL\n"
    "FROM pg_catalog.pg_publication p\n"
    "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
    "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,35 +3039,62 @@ describeOneTableDetails(const char *schemaname,
    "                pg_catalog.pg_attribute\n"
    "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
    "        ELSE NULL END) "
+   " , prexcept "
    "FROM pg_catalog.pg_publication p\n"
    " JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
    " JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-   "WHERE pr.prrelid = '%s'\n",
-   oid, oid, oid);
-
- if (pset.sversion >= 190000)
- appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+   "WHERE pr.prrelid = '%s' "
+   "AND  c.relnamespace NOT IN (\n "
+   " SELECT pnnspid FROM\n"
+   " pg_catalog.pg_publication_namespace)\n"
- appendPQExpBuffer(&buf,
    "UNION\n"
    "SELECT pubname\n"
    " , NULL\n"
    " , NULL\n"
+   " , NULL\n"
    "FROM pg_catalog.pg_publication p\n"
-   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-   oid);
-
- if (pset.sversion >= 190000)
- appendPQExpBuffer(&buf,
-   "     AND NOT EXISTS (\n"
-   " SELECT 1\n"
-   " FROM pg_catalog.pg_publication_rel pr\n"
-   " JOIN pg_catalog.pg_class pc\n"
-   " ON pr.prrelid = pc.oid\n"
-   " WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-   oid);
-
- appendPQExpBufferStr(&buf, "ORDER BY 1;");
+   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+   "     AND NOT EXISTS (\n"
+   " SELECT 1\n"
+   " FROM pg_catalog.pg_publication_rel pr\n"
+   " JOIN pg_catalog.pg_class pc\n"
+   " ON pr.prrelid = pc.oid\n"
+   " WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+   "ORDER BY 1;",
+   oid, oid, oid, oid, oid);
+ }
+ else if (pset.sversion >= 150000)
+ {
+ printfPQExpBuffer(&buf,
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+   "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+   "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , pg_get_expr(pr.prqual, c.oid)\n"
+   "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+   "         (SELECT string_agg(attname, ', ')\n"
+   "           FROM pg_catalog.generate_series(0,
pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+   "                pg_catalog.pg_attribute\n"
+   "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+   "        ELSE NULL END) "
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+   "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+   "WHERE pr.prrelid = '%s'\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+   "ORDER BY 1;",
+   oid, oid, oid, oid);

I found these large SQL selects with 3x UNIONs are difficult to read.
Maybe you can add more comments to describe the intention of each of
the UNION SELECTs?

~~~

9.
  /* column list (if any) */
  if (!PQgetisnull(result, i, 2))
- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT");
+ appendPQExpBuffer(&buf, " (%s)", PQgetvalue(result, i, 2));
+ }

I did not find any regression test case where the "EXCEPT" col-list is
getting output for a "Publications:" footer.

======
[1]: /messages/by-id/aIELRMAviNiUL1ie@momjian.us

Kind Regards,
Peter Smith.
Fujitsu Australia

#112Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#111)
3 attachment(s)
Re: Skipping schema changes in publication

On Mon, 11 Aug 2025 at 13:55, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

On Wed, Aug 6, 2025 at 11:11 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

...

5.
Bitmapset  *cols = NULL;
+ bool except_columns = false;
+ bool no_col_published = false;

There are multiple places in this patch that say:

'no_col_published'
or 'no_cols_published'

I felt this var name can be misunderstood because it is easy to read
"no" as meaning "no." (aka number), and then misinterpret as
"number_of_cols_published".

Maybe an unambiguous name can be found, like
- 'zero_cols_published' or
- 'nothing_published' or
- really make it 'num_cols_published' and check for 0.

(so this comment applies to multiple places in the patch)

How about 'all_cols_excluded'? Or 'has_published_cols'?
I have used 'all_cols_excluded' in this patch. Thoughts?

The new name is good.

======
src/bin/psql/describe.c

describeOneTableDetails:

7.
/* column list (if any) */
if (!PQgetisnull(result, i, 2))
- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT (%s)",
+   PQgetvalue(result, i, 2));
+ else
+ appendPQExpBuffer(&buf, " (%s)",
+   PQgetvalue(result, i, 2));
+ }

Isn't this code fragment (and also surrounding code) using the same
logic as what is already encapsulated in the function
addFooterToPublicationDesc()?
Superficially, it seems like a large chunk can all be replaced with a
single call to the existing function.

'addFooterToPublicationDesc' is called when we use \dRp+ and print in format:
"schema_name.table_name" EXCEPT (column-list)
Whereas code pasted above is executed when we use \d+ table_name and
the output is the format:
"publication_name" EXCEPT (column-list)

These pieces of code are used to print different info. One is used to
print info related to tables and the other is used to print info
related to publication.
Should we use a common function for this?

It still seems like quite a lot of overlap. e.g. I thought there were
~30 lines common. OTOH, perhaps you'll need to pass another boolean to
the function to indicate it is a "Publication:" footer. I guess you'd
have to try it out first to see if the changes required to save those
30 LOC are worthwhile or not.

I have added the code changes for the same in this patch.

======
src/test/regress/expected/publication.out

8.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  EXCEPT clause not allowed for table without column list
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^

Is that a bad syntax position marker (^)? e.g. Why is it pointed at
the word "TABLE" instead of "EXCEPT"?

In function 'preprocess_pubobj_list' the position of position marker
(^) is decided by "pubobj->location". Function handles multiple errors
and setting "$$->location" only specific to EXCEPT qualifier would not
be appropriate. One solution I feel is to not show "position marker
(^)" in the case of EXCEPT. Or maybe we can add a new variable to
'PublicationTable' for except_location but I think we should not do
that. Thoughts?

In the review comments below, I suggest putting this location back,
but changing the message.

For this version of patch, I have removed the "position marker (^)" in
the case of EXCEPT.

//////

Here are my review comments for the patch v19-0003.

======
1. General - SGML tags in docs for table/column names.

There is nothing to change just yet, but keep an eye on the thread
[1], because if/when that gets pushed, then there will several tags
in this patch for table/column names that will need to be updated for
consistency.

Noted

======
src/backend/catalog/pg_publication.c

pg_get_publication_tables:

2.
+
+ if (!nulls[2])
+ {
+ Datum exceptDatum;
+ bool isnull;
+
+ /*
+ * We fetch pubtuple if publication is not FOR ALL TABLES and
+ * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+ * indicates that prattrs contains columns to be excluded for
+ * replication.
+ */
+ exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+   Anum_pg_publication_rel_prexcept,
+   &isnull);
+
+ if (!isnull && DatumGetBool(exceptDatum))
+ except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+ }

Maybe this should be done a few lines earlier, to keep all the
values[2]/nulls[2] code together, ahead of the values[3]/nulls[3]
code. Indeed, there is lots of other values[2]/nulls[2] logic that
comes later in this function, so maybe it is better to do all of that
first, instead of mingling it with values[3]/nulls[3].

======
src/backend/commands/publicationcmds.c

pub_contains_invalid_column:

3.
* 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
+ *    by the column list and are not part of column list specified with EXCEPT.
+ *   If any column is missing, *invalid_column_list is set
*    to true.

Whitespace problem here; there is some tab instead of space in this comment.

Also /part of column list/part of the column list/

~~~

AlterPublicationTables:

4.
bool isnull = true;
Datum whereClauseDatum;
Datum columnListDatum;
+ Datum exceptDatum;

It's not necessary to have all these different Datum variables; they
are only temporary storage. It might be simpler to use a single "Datum
datum;" which is reused 3x.

~

5.
+ exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+   Anum_pg_publication_rel_prexcept,
+   &isnull);
+
+ if (!isnull)
+ oldexcept = DatumGetBool(exceptDatum);
+

Isn't the 'prexcept' also used for EXCEPT TABLE as well as EXCEPT
(column-list)? In other words, should the change to this function be
done already in one of the earlier patches?

~

This code path is only executed when running ALTER PUBLICATION ... SET
TABLE and running this command on a ALL TABLES publication throws an
error due to check by function 'CheckAlterPublication' . And EXCEPT
TABLE can only be used for ALL TABLES publications, I think it doesn’t
need to be moved to the 0002 patch.

6.
if (equal(oldrelwhereclause, newpubrel->whereClause) &&
- bms_equal(oldcolumns, newcolumns))
+ bms_equal(oldcolumns, newcolumns) &&
+ oldexcept == newpubrel->except)

The code comment about this code fragment should also mention EXCEPT.

======
src/backend/parser/gram.y

preprocess_pubobj_list:

7.
+ if (pubobj->pubtable && pubobj->pubtable->except &&
+ pubobj->pubtable->columns == NULL)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("EXCEPT clause not allowed for table without column list"));
+

Having the syntax error location (like before in v18) might be better,
but since that location is associated with the TABLE, then the error
message should also be reworded so the subject is the table.

SUGGESTION
errmsg("table without column list cannot use EXCEPT clause")

======
src/bin/psql/describe.c

describeOneTableDetails:

8.
- if (pset.sversion >= 150000)
+ if (pset.sversion >= 190000)
{
printfPQExpBuffer(&buf,
"SELECT pubname\n"
"     , NULL\n"
"     , NULL\n"
+   " , NULL\n"
"FROM pg_catalog.pg_publication p\n"
"     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
"     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
@@ -3038,35 +3039,62 @@ describeOneTableDetails(const char *schemaname,
"                pg_catalog.pg_attribute\n"
"          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
"        ELSE NULL END) "
+   " , prexcept "
"FROM pg_catalog.pg_publication p\n"
" JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
" JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-   "WHERE pr.prrelid = '%s'\n",
-   oid, oid, oid);
-
- if (pset.sversion >= 190000)
- appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+   "WHERE pr.prrelid = '%s' "
+   "AND  c.relnamespace NOT IN (\n "
+   " SELECT pnnspid FROM\n"
+   " pg_catalog.pg_publication_namespace)\n"
- appendPQExpBuffer(&buf,
"UNION\n"
"SELECT pubname\n"
" , NULL\n"
" , NULL\n"
+   " , NULL\n"
"FROM pg_catalog.pg_publication p\n"
-   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-   oid);
-
- if (pset.sversion >= 190000)
- appendPQExpBuffer(&buf,
-   "     AND NOT EXISTS (\n"
-   " SELECT 1\n"
-   " FROM pg_catalog.pg_publication_rel pr\n"
-   " JOIN pg_catalog.pg_class pc\n"
-   " ON pr.prrelid = pc.oid\n"
-   " WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-   oid);
-
- appendPQExpBufferStr(&buf, "ORDER BY 1;");
+   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+   "     AND NOT EXISTS (\n"
+   " SELECT 1\n"
+   " FROM pg_catalog.pg_publication_rel pr\n"
+   " JOIN pg_catalog.pg_class pc\n"
+   " ON pr.prrelid = pc.oid\n"
+   " WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+   "ORDER BY 1;",
+   oid, oid, oid, oid, oid);
+ }
+ else if (pset.sversion >= 150000)
+ {
+ printfPQExpBuffer(&buf,
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+   "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+   "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , pg_get_expr(pr.prqual, c.oid)\n"
+   "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+   "         (SELECT string_agg(attname, ', ')\n"
+   "           FROM pg_catalog.generate_series(0,
pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+   "                pg_catalog.pg_attribute\n"
+   "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+   "        ELSE NULL END) "
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+   "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+   "WHERE pr.prrelid = '%s'\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+   "ORDER BY 1;",
+   oid, oid, oid, oid);

I found these large SQL selects with 3x UNIONs are difficult to read.
Maybe you can add more comments to describe the intention of each of
the UNION SELECTs?

~~~

9.
/* column list (if any) */
if (!PQgetisnull(result, i, 2))
- appendPQExpBuffer(&buf, " (%s)",
-   PQgetvalue(result, i, 2));
+ {
+ if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ appendPQExpBuffer(&buf, " EXCEPT");
+ appendPQExpBuffer(&buf, " (%s)", PQgetvalue(result, i, 2));
+ }

I did not find any regression test case where the "EXCEPT" col-list is
getting output for a "Publications:" footer.

======
[1] /messages/by-id/aIELRMAviNiUL1ie@momjian.us

I have addressed the comments and the changes in v20 patch.

Thanks,
Shlok Kyal

Attachments:

v20-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v20-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 02a57021a52e062d4b7d34b6c3279bbacba60ed7 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v20 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 803c26ab216..06f6f45526b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1603,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..4a4010296af 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10850,6 +10850,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10896,6 +10898,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b10f2313f3..f07af7f71d3 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,7 +2266,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 53268059142..74009a92f3f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index deddf0da844..1366b11bba0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v20-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v20-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From cb3e89895a4335d9f1796272cf3465ea72784ab8 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v20 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 ++++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 +++++-
 src/backend/commands/publicationcmds.c        |  54 ++--
 src/backend/parser/gram.y                     |  44 ++--
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 +++++-
 src/bin/pg_dump/pg_dump.c                     |  45 ++--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 237 ++++++++++--------
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 +++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 190 ++++++++++++++
 19 files changed, 875 insertions(+), 214 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e96a55fecf9..c14077caa68 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 414a314acc5..2f04f93620e 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index af1b8c9ed67..d21b3ff48e1 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1282,6 +1306,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,11 +1331,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1322,8 +1365,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1401,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1416,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b6d546be291..8813fb28576 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,8 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -381,6 +380,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +404,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +495,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1452,6 +1459,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1465,23 +1473,30 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
+
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1508,13 +1523,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 49bac034d17..cebd7c2a3c4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4450,6 +4450,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10759,14 +10764,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10782,7 +10788,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10790,7 +10796,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10800,8 +10806,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10809,25 +10816,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19695,6 +19704,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..68ff559e80c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -821,10 +821,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -838,6 +846,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -888,7 +899,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +913,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +933,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +980,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1045,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1178,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b9d676d1f18..1cf90f1875d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,19 +1141,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1187,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1197,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2109,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2159,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 105031a5cbc..a57ba69f748 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4865,24 +4865,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4908,10 +4891,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4991,7 +4993,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c178edb4e05..5e2aa1b0cf0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f3fe4ab30f8..6b727458076 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,68 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * If is_tbl_desc is true add footer to table description else add footer to
+ * publication description.
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf, const char *footermsg,
+								  bool as_schema, printTableContent *const cont,
+								  bool is_tbl_desc)
+{
+	PGresult   *res;
+	int			count = 0;
+	int			i = 0;
+	int			col = is_tbl_desc ? 0 : 1;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+	else
+		count = PQntuples(res);
+
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*---------------------------------------------------
+	 * Publication/ table description columns:
+	 * [0]: schema name (nspname)
+	 * [col]: table name (relname) / publication name (pubname)
+	 * [col + 1]: row filter expression (prqual), may be NULL
+	 * [col + 2]: column list (comma-separated), may be NULL
+	 * [col + 3]: except flag ("t" if EXCEPT, else "f")
+	 *---------------------------------------------------
+	 */
+	for (i = 0; i < count; i++)
+	{
+		if (as_schema)
+			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
+		else
+		{
+			if (is_tbl_desc)
+				printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, col));
+			else
+				printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3019,16 +3081,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3038,35 +3111,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3087,34 +3192,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, _("Publications:"), false, &cont, true))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6491,49 +6570,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6704,6 +6740,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,11 +6754,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, _("Tables:"), false, &cont, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6728,8 +6767,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, _("Tables from schemas:"),
+													   true, &cont, false))
 					goto error_return;
 			}
 		}
@@ -6745,8 +6784,8 @@ describePublications(const char *pattern)
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, _("Except tables:"),
+													   true, &cont, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 5efdcf56347..dd560c9ba8c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2272,6 +2272,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3601,7 +3603,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 49afeb77622..69404c6aa1f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,94 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6240cd97ce3..bf64e8a3ce1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,61 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..79e63c0f449
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,190 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED, bgen int GENERATED ALWAYS AS (2) STORED);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+	CREATE TABLE tab6 (agen int, bgen int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'check initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'check initial sync for EXCEPT (column-list) publication');
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column are is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v20-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v20-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 23512b346d13bb9f1bbff2d9e5e43357edb1a553 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v20 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 215 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 858 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da8a7882580..e96a55fecf9 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index a0761cfee3f..414a314acc5 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2299,10 +2299,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..ca8f6dc9b9f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index b911efcf9cb..af1b8c9ed67 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 06f6f45526b..b6d546be291 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -525,7 +532,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1028,7 +1033,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1148,7 +1153,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1271,7 +1297,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1289,6 +1318,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1327,7 +1429,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1428,6 +1531,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1478,7 +1582,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1601,6 +1706,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1813,6 +1932,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1885,6 +2005,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1960,8 +2081,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c6dd2e020da..8f3b810a594 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a4010296af..49bac034d17 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10702,7 +10703,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10722,12 +10723,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10765,6 +10767,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10840,6 +10843,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10852,6 +10874,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10878,6 +10902,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 80540c017bd..b9d676d1f18 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 6fe268a8eec..ebcf6d3bd32 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index fc7a6639163..105031a5cbc 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4597,8 +4599,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4764,6 +4792,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4775,8 +4804,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4787,6 +4824,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4802,6 +4840,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4813,6 +4852,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4826,7 +4866,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4867,6 +4911,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11729,6 +11776,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20095,6 +20145,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..c178edb4e05 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index a02da3e9652..40fdfcb121c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -429,6 +431,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1701,6 +1714,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e7a2d64f741..7e4e589919b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3354,6 +3354,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7a06af48842..f3fe4ab30f8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index f07af7f71d3..5efdcf56347 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,11 +2269,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3588,6 +3593,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 74009a92f3f..49afeb77622 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1964,6 +2028,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1982,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1999,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2037,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 1366b11bba0..6240cd97ce3 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,17 +1238,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1243,6 +1269,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#113Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#112)
1 attachment(s)
Re: Skipping schema changes in publication

Hi Shlok,

Here are some review comments for v20-0003.

======
src/backend/commands/publicationcmds.c

AlterPublicationTables:

1.
  bool isnull = true;
- Datum whereClauseDatum;
- Datum columnListDatum;
+ Datum datum;

I know you did not write the code, but that "isnull = true" is
redundant, and seems kind of misleading because it will always be
re-assigned before it is used.

~~~

2.
  /* Load the WHERE clause for this table. */
- whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-    Anum_pg_publication_rel_prqual,
-    &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prqual,
+ &isnull);
  if (!isnull)
- oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+ oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
  /* Transform the int2vector column list to a bitmap. */
- columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-   Anum_pg_publication_rel_prattrs,
-   &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prattrs,
+ &isnull);
+
+ if (!isnull)
+ oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
+
+ /* Load the prexcept flag for this table. */
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prexcept,
+ &isnull);
  if (!isnull)
- oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+ oldexcept = DatumGetBool(datum);

Use consistent spacing. Either do or don't (I prefer don't) put a
blank line between the pairs of "datum =" and "if (!isnull)". Avoid
having a mixture.

======
src/bin/psql/describe.c

addFooterToPublicationOrTableDesc:

3.
+/*
+ * If is_tbl_desc is true add footer to table description else add footer to
+ * publication description.
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf, const char *footermsg,
+   bool as_schema, printTableContent *const cont,
+   bool is_tbl_desc)

3a.
Since you are changing this anyway, I think it would be better to keep
those boolean params together (at the end).

~

3b.
It seems a bit mixed up calling this addFooterToPublicationOrTableDesc
but having the variable 'is_tbl_desc', because it seems more natural
to me to read left to right, so the logical order of everything here
should be pub desc then table desc. In other words, use boolean
'is_pub_desc' instead of 'is_tbl_desc'. Also, I think that 'as_schema'
thing is kind of a *subset* of the publication description, so it
makes more sense for that to come last too.

e.g.
CURRENT
addFooterToPublicationOrTableDesc(buf, footermsg, as_schema, cont, is_tbl_desc)
SUGGESTION
addFooterToPublicationOrTableDesc(buf, cont, footermsg, is_pub_desc, as_schema)

~

3c
While you are changing things, maybe also consider changing that
'as_schema' name because I did not understand what "as" means. Perhaps
rename like 'pub_schemas', or 'only_show_schemas' or something better
(???).

~~~

4.
+ PGresult   *res;
+ int count = 0;
+ int i = 0;
+ int col = is_tbl_desc ? 0 : 1;
+
+ res = PSQLexec(buf->data);
+ if (!res)
+ return false;
+ else
+ count = PQntuples(res);
+

4a.
Assignment count = 0 is redundant.

~

4b.
Remove the 'i' declaration here. Declare it in the "for" loop later.

~

4c.
The "else" is not required. If 'res' was not good, you already returned.

~~~

5.
+ if (as_schema)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
+ else
+ {
+ if (is_tbl_desc)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, col));
+ else
+ printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
+   PQgetvalue(res, i, col));

This function is basically either (a) a footer for a table description
or (b) a footer for a publication description. And that all hinges on
the boolean 'is_tbl_desc'. Therefore, it seems more natural for the
main condition to be "if (is_tbl_desc)" here.

This turned everything inside out. PSA: a top-up patch to show a way
to do this. Perhaps my implementation is a bit verbose, but OTOH it
seems easier to understand. Anyway, see what you think...

~~~

6.
+ /*---------------------------------------------------
+ * Publication/ table description columns:
+ * [0]: schema name (nspname)
+ * [col]: table name (relname) / publication name (pubname)
+ * [col + 1]: row filter expression (prqual), may be NULL
+ * [col + 2]: column list (comma-separated), may be NULL
+ * [col + 3]: except flag ("t" if EXCEPT, else "f")
+ *---------------------------------------------------

I've modified this comment slightly so I could understand it better.
See if you agree.

SUGGESTION
/*---------------------------------------------------
* Description columns:
* PUB TBL
* [0] - : schema name (nspname)
* [col] - : table name (relname)
* - [col] : publication name (pubname)
* [col+1] [col+1]: row filter expression (prqual), may be NULL
* [col+2] [col+1]: column list (comma-separated), may be NULL
* [col+3] [col+1]: except flag ("t" if EXCEPT, else "f")
*---------------------------------------------------
*/

~~~

describeOneTableDetails:

7.
+ else if (pset.sversion >= 150000)
+ {
+ printfPQExpBuffer(&buf,
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+   "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+   "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , pg_get_expr(pr.prqual, c.oid)\n"
+   "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+   "         (SELECT string_agg(attname, ', ')\n"
+   "           FROM pg_catalog.generate_series(0,
pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+   "                pg_catalog.pg_attribute\n"
+   "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+   "        ELSE NULL END) "
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+   "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+   "WHERE pr.prrelid = '%s'\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+   "ORDER BY 1;",
+   oid, oid, oid, oid);

AFAICT, that >= 150000 code seems to have added another UNION at the
end that was not previously there. What's that about? How is that
related to EXCEPT (column-list)?

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

Attachments:

PS_addFooterToPublicationOrTableDesc.diffapplication/octet-stream; name=PS_addFooterToPublicationOrTableDesc.diffDownload
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 6b72745..482cca6 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1561,49 +1561,69 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 }
 
 /*
- * If is_tbl_desc is true add footer to table description else add footer to
- * publication description.
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'as_schema' - true if the pub_desc only shows schemas, otherwise false
  */
 static bool
-addFooterToPublicationOrTableDesc(PQExpBuffer buf, const char *footermsg,
-								  bool as_schema, printTableContent *const cont,
-								  bool is_tbl_desc)
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool as_schema)
 {
 	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-	int			col = is_tbl_desc ? 0 : 1;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
 
 	res = PSQLexec(buf->data);
 	if (!res)
 		return false;
-	else
-		count = PQntuples(res);
 
+	count = PQntuples(res);
 	if (count > 0)
 		printTableAddFooter(cont, footermsg);
 
-	/*---------------------------------------------------
-	 * Publication/ table description columns:
-	 * [0]: schema name (nspname)
-	 * [col]: table name (relname) / publication name (pubname)
-	 * [col + 1]: row filter expression (prqual), may be NULL
-	 * [col + 2]: column list (comma-separated), may be NULL
-	 * [col + 3]: except flag ("t" if EXCEPT, else "f")
-	 *---------------------------------------------------
+	/*--------------------------------------------------------------
+	 * Description columns:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+1]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+1]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
 	 */
-	for (i = 0; i < count; i++)
+	for (int i = 0; i < count; i++)
 	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/* Footers entries for a publication description or a table description */
+		if (is_pub_desc)
 		{
-			if (is_tbl_desc)
-				printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, col));
+			if (as_schema)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
 			else
-				printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
 								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
 
+		/* Common footer output for column list and/or row filter */
+		if (!as_schema)
+		{
 			if (!PQgetisnull(res, i, col + 2))
 			{
 				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
@@ -3192,7 +3212,7 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			if (!addFooterToPublicationOrTableDesc(&buf, _("Publications:"), false, &cont, true))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
 		}
 
@@ -6755,7 +6775,7 @@ describePublications(const char *pattern)
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationOrTableDesc(&buf, _("Tables:"), false, &cont, false))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6767,8 +6787,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationOrTableDesc(&buf, _("Tables from schemas:"),
-													   true, &cont, false))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6784,8 +6804,8 @@ describePublications(const char *pattern)
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationOrTableDesc(&buf, _("Except tables:"),
-													   true, &cont, false))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, true))
 					goto error_return;
 			}
 		}
#114Kirill Reshke
reshkekirill@gmail.com
In reply to: Peter Smith (#113)
Re: Skipping schema changes in publication

Hi

On Fri, 15 Aug 2025 at 05:53, Peter Smith <smithpb2250@gmail.com> wrote:

1.
bool isnull = true;
- Datum whereClauseDatum;
- Datum columnListDatum;
+ Datum datum;

I know you did not write the code, but that "isnull = true" is
redundant, and seems kind of misleading because it will always be
re-assigned before it is used.

People are not generally excited about refactoring code they did not
change. This makes patch to have more review cycles, and less probable
to actually being committed. If we are really wedded with this change,
this could be a separate thread.

~~~

2.
/* Load the WHERE clause for this table. */
- whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-    Anum_pg_publication_rel_prqual,
-    &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prqual,
+ &isnull);
if (!isnull)
- oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+ oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
/* Transform the int2vector column list to a bitmap. */
- columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-   Anum_pg_publication_rel_prattrs,
-   &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prattrs,
+ &isnull);
+
+ if (!isnull)
+ oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
+
+ /* Load the prexcept flag for this table. */
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prexcept,
+ &isnull);
if (!isnull)
- oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+ oldexcept = DatumGetBool(datum);

Use consistent spacing. Either do or don't (I prefer don't) put a
blank line between the pairs of "datum =" and "if (!isnull)". Avoid
having a mixture.

======
src/bin/psql/describe.c

addFooterToPublicationOrTableDesc:

3.
+/*
+ * If is_tbl_desc is true add footer to table description else add footer to
+ * publication description.
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf, const char *footermsg,
+   bool as_schema, printTableContent *const cont,
+   bool is_tbl_desc)

3a.
Since you are changing this anyway, I think it would be better to keep
those boolean params together (at the end).

~

3b.
It seems a bit mixed up calling this addFooterToPublicationOrTableDesc
but having the variable 'is_tbl_desc', because it seems more natural
to me to read left to right, so the logical order of everything here
should be pub desc then table desc. In other words, use boolean
'is_pub_desc' instead of 'is_tbl_desc'. Also, I think that 'as_schema'
thing is kind of a *subset* of the publication description, so it
makes more sense for that to come last too.

e.g.
CURRENT
addFooterToPublicationOrTableDesc(buf, footermsg, as_schema, cont, is_tbl_desc)
SUGGESTION
addFooterToPublicationOrTableDesc(buf, cont, footermsg, is_pub_desc, as_schema)

~

3c
While you are changing things, maybe also consider changing that
'as_schema' name because I did not understand what "as" means. Perhaps
rename like 'pub_schemas', or 'only_show_schemas' or something better
(???).

~~~

4.
+ PGresult   *res;
+ int count = 0;
+ int i = 0;
+ int col = is_tbl_desc ? 0 : 1;
+
+ res = PSQLexec(buf->data);
+ if (!res)
+ return false;
+ else
+ count = PQntuples(res);
+

4a.
Assignment count = 0 is redundant.

~

4b.
Remove the 'i' declaration here. Declare it in the "for" loop later.

~

4c.
The "else" is not required. If 'res' was not good, you already returned.

~~~

5.
+ if (as_schema)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
+ else
+ {
+ if (is_tbl_desc)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, col));
+ else
+ printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
+   PQgetvalue(res, i, col));

This function is basically either (a) a footer for a table description
or (b) a footer for a publication description. And that all hinges on
the boolean 'is_tbl_desc'. Therefore, it seems more natural for the
main condition to be "if (is_tbl_desc)" here.

This turned everything inside out. PSA: a top-up patch to show a way
to do this. Perhaps my implementation is a bit verbose, but OTOH it
seems easier to understand. Anyway, see what you think...

+ 1

6.
+ /*---------------------------------------------------
+ * Publication/ table description columns:
+ * [0]: schema name (nspname)
+ * [col]: table name (relname) / publication name (pubname)
+ * [col + 1]: row filter expression (prqual), may be NULL
+ * [col + 2]: column list (comma-separated), may be NULL
+ * [col + 3]: except flag ("t" if EXCEPT, else "f")
+ *---------------------------------------------------

I've modified this comment slightly so I could understand it better.
See if you agree.

For me that's equal. lets see what other people think

--
Best regards,
Kirill Reshke

#115Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#113)
3 attachment(s)
Re: Skipping schema changes in publication

On Fri, 15 Aug 2025 at 06:23, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

Here are some review comments for v20-0003.

======
src/backend/commands/publicationcmds.c

AlterPublicationTables:

1.
bool isnull = true;
- Datum whereClauseDatum;
- Datum columnListDatum;
+ Datum datum;

I know you did not write the code, but that "isnull = true" is
redundant, and seems kind of misleading because it will always be
re-assigned before it is used.

Since this is part of already existing code, I think this should be a
new thread. I have created a new thread for this. See [1]/messages/by-id/CANhcyEXHiCbk2q8=bq3boQDyc8ac9fjgK-kkp5PdTYLcAOq80Q@mail.gmail.com.

~~~

2.
/* Load the WHERE clause for this table. */
- whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-    Anum_pg_publication_rel_prqual,
-    &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prqual,
+ &isnull);
if (!isnull)
- oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+ oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
/* Transform the int2vector column list to a bitmap. */
- columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-   Anum_pg_publication_rel_prattrs,
-   &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prattrs,
+ &isnull);
+
+ if (!isnull)
+ oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
+
+ /* Load the prexcept flag for this table. */
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prexcept,
+ &isnull);
if (!isnull)
- oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+ oldexcept = DatumGetBool(datum);

Use consistent spacing. Either do or don't (I prefer don't) put a
blank line between the pairs of "datum =" and "if (!isnull)". Avoid
having a mixture.

======
src/bin/psql/describe.c

addFooterToPublicationOrTableDesc:

3.
+/*
+ * If is_tbl_desc is true add footer to table description else add footer to
+ * publication description.
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf, const char *footermsg,
+   bool as_schema, printTableContent *const cont,
+   bool is_tbl_desc)

3a.
Since you are changing this anyway, I think it would be better to keep
those boolean params together (at the end).

~

3b.
It seems a bit mixed up calling this addFooterToPublicationOrTableDesc
but having the variable 'is_tbl_desc', because it seems more natural
to me to read left to right, so the logical order of everything here
should be pub desc then table desc. In other words, use boolean
'is_pub_desc' instead of 'is_tbl_desc'. Also, I think that 'as_schema'
thing is kind of a *subset* of the publication description, so it
makes more sense for that to come last too.

e.g.
CURRENT
addFooterToPublicationOrTableDesc(buf, footermsg, as_schema, cont, is_tbl_desc)
SUGGESTION
addFooterToPublicationOrTableDesc(buf, cont, footermsg, is_pub_desc, as_schema)

~

3c
While you are changing things, maybe also consider changing that
'as_schema' name because I did not understand what "as" means. Perhaps
rename like 'pub_schemas', or 'only_show_schemas' or something better
(???).

I have used pub_schemas.

~~~

4.
+ PGresult   *res;
+ int count = 0;
+ int i = 0;
+ int col = is_tbl_desc ? 0 : 1;
+
+ res = PSQLexec(buf->data);
+ if (!res)
+ return false;
+ else
+ count = PQntuples(res);
+

4a.
Assignment count = 0 is redundant.

~

4b.
Remove the 'i' declaration here. Declare it in the "for" loop later.

~

4c.
The "else" is not required. If 'res' was not good, you already returned.

~~~

5.
+ if (as_schema)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
+ else
+ {
+ if (is_tbl_desc)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, col));
+ else
+ printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
+   PQgetvalue(res, i, col));

This function is basically either (a) a footer for a table description
or (b) a footer for a publication description. And that all hinges on
the boolean 'is_tbl_desc'. Therefore, it seems more natural for the
main condition to be "if (is_tbl_desc)" here.

This turned everything inside out. PSA: a top-up patch to show a way
to do this. Perhaps my implementation is a bit verbose, but OTOH it
seems easier to understand. Anyway, see what you think...

I have also used the patch with minor changes.

~~~

6.
+ /*---------------------------------------------------
+ * Publication/ table description columns:
+ * [0]: schema name (nspname)
+ * [col]: table name (relname) / publication name (pubname)
+ * [col + 1]: row filter expression (prqual), may be NULL
+ * [col + 2]: column list (comma-separated), may be NULL
+ * [col + 3]: except flag ("t" if EXCEPT, else "f")
+ *---------------------------------------------------

I've modified this comment slightly so I could understand it better.
See if you agree.

SUGGESTION
/*---------------------------------------------------
* Description columns:
* PUB TBL
* [0] - : schema name (nspname)
* [col] - : table name (relname)
* - [col] : publication name (pubname)
* [col+1] [col+1]: row filter expression (prqual), may be NULL
* [col+2] [col+1]: column list (comma-separated), may be NULL
* [col+3] [col+1]: except flag ("t" if EXCEPT, else "f")
*---------------------------------------------------
*/

~~~

I have used the suggested description with some modifications.

describeOneTableDetails:

7.
+ else if (pset.sversion >= 150000)
+ {
+ printfPQExpBuffer(&buf,
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+   "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+   "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , pg_get_expr(pr.prqual, c.oid)\n"
+   "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+   "         (SELECT string_agg(attname, ', ')\n"
+   "           FROM pg_catalog.generate_series(0,
pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+   "                pg_catalog.pg_attribute\n"
+   "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+   "        ELSE NULL END) "
+   "FROM pg_catalog.pg_publication p\n"
+   "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+   "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+   "WHERE pr.prrelid = '%s'\n"
+   "UNION\n"
+   "SELECT pubname\n"
+   "     , NULL\n"
+   "     , NULL\n"
+   "FROM pg_catalog.pg_publication p\n"
+   "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+   "ORDER BY 1;",
+   oid, oid, oid, oid);

AFAICT, that >= 150000 code seems to have added another UNION at the
end that was not previously there. What's that about? How is that
related to EXCEPT (column-list)?

This patch does not add any new code to >= 150000. It is the same as
HEAD. This diff appears because of changes in 0002 patchset. In patch
0002, I did not create a separate full query for >= 190000 due to
small changes.

I have addressed the rest of the comments and added the changes in the
latest v21 patchset.

[1]: /messages/by-id/CANhcyEXHiCbk2q8=bq3boQDyc8ac9fjgK-kkp5PdTYLcAOq80Q@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v21-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v21-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 0f54a46a8f3ef8f6d4e252a10fd4bed7edb557ca Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v21 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 190 +++++++++++++
 19 files changed, 897 insertions(+), 215 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e96a55fecf9..c14077caa68 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 7e9e27aba4e..930cae7034f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index af1b8c9ed67..d21b3ff48e1 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1282,6 +1306,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,11 +1331,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1322,8 +1365,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1401,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1416,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b6d546be291..db824d25d74 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,8 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -381,6 +380,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +404,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +495,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1452,6 +1459,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1465,23 +1473,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1508,13 +1521,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 49bac034d17..cebd7c2a3c4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4450,6 +4450,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10759,14 +10764,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10782,7 +10788,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10790,7 +10796,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10800,8 +10806,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10809,25 +10816,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19695,6 +19704,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..68ff559e80c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -821,10 +821,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -838,6 +846,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -888,7 +899,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +913,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +933,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +980,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1045,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1178,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b9d676d1f18..1cf90f1875d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,19 +1141,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1187,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1197,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2109,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2159,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 105031a5cbc..a57ba69f748 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4865,24 +4865,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4908,10 +4891,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4991,7 +4993,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c178edb4e05..5e2aa1b0cf0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f3fe4ab30f8..9f8906730f1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footers entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3019,16 +3104,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3038,35 +3134,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3087,34 +3215,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6491,49 +6593,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6704,6 +6763,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,11 +6777,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6728,8 +6790,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6739,14 +6801,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 5efdcf56347..dd560c9ba8c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2272,6 +2272,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3601,7 +3603,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 49afeb77622..69404c6aa1f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,94 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6240cd97ce3..bf64e8a3ce1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,61 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..79e63c0f449
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,190 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED, bgen int GENERATED ALWAYS AS (2) STORED);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+	CREATE TABLE tab6 (agen int, bgen int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'check initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'check initial sync for EXCEPT (column-list) publication');
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column are is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v21-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v21-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From e73312a6eae422404d67f413f009cbf8ef1d2b21 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v21 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 803c26ab216..06f6f45526b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1603,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..4a4010296af 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10850,6 +10850,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10896,6 +10898,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b10f2313f3..f07af7f71d3 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,7 +2266,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 53268059142..74009a92f3f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index deddf0da844..1366b11bba0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

v21-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v21-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 51b06cd8b583d948e55bf6aa25f31029e5593560 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v21 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 215 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 858 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da8a7882580..e96a55fecf9 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 0ac29928f17..7e9e27aba4e 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2299,10 +2299,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..ca8f6dc9b9f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index b911efcf9cb..af1b8c9ed67 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 06f6f45526b..b6d546be291 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -525,7 +532,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1028,7 +1033,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1148,7 +1153,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1271,7 +1297,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1289,6 +1318,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1327,7 +1429,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1428,6 +1531,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1478,7 +1582,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1601,6 +1706,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1813,6 +1932,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1885,6 +2005,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1960,8 +2081,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c6dd2e020da..8f3b810a594 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a4010296af..49bac034d17 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10702,7 +10703,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10722,12 +10723,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10765,6 +10767,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10840,6 +10843,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10852,6 +10874,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10878,6 +10902,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 80540c017bd..b9d676d1f18 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 6fe268a8eec..ebcf6d3bd32 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index fc7a6639163..105031a5cbc 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4597,8 +4599,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4764,6 +4792,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4775,8 +4804,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4787,6 +4824,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4802,6 +4840,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4813,6 +4852,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4826,7 +4866,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4867,6 +4911,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11729,6 +11776,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20095,6 +20145,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..c178edb4e05 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index a02da3e9652..40fdfcb121c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -429,6 +431,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1701,6 +1714,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e7a2d64f741..7e4e589919b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3354,6 +3354,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7a06af48842..f3fe4ab30f8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index f07af7f71d3..5efdcf56347 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,11 +2269,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3588,6 +3593,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 74009a92f3f..49afeb77622 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1964,6 +2028,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1982,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1999,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2037,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 1366b11bba0..6240cd97ce3 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,17 +1238,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1243,6 +1269,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#116Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Kirill Reshke (#114)
Re: Skipping schema changes in publication

Hi Kirill,

Thanks for reviewing the patch.

On Fri, 15 Aug 2025 at 11:46, Kirill Reshke <reshkekirill@gmail.com> wrote:

Hi

On Fri, 15 Aug 2025 at 05:53, Peter Smith <smithpb2250@gmail.com> wrote:

1.
bool isnull = true;
- Datum whereClauseDatum;
- Datum columnListDatum;
+ Datum datum;

I know you did not write the code, but that "isnull = true" is
redundant, and seems kind of misleading because it will always be
re-assigned before it is used.

People are not generally excited about refactoring code they did not
change. This makes patch to have more review cycles, and less probable
to actually being committed. If we are really wedded with this change,
this could be a separate thread.

I also feel that we should create a new thread for the same. I have
created a new thread. See [1]/messages/by-id/CANhcyEXHiCbk2q8=bq3boQDyc8ac9fjgK-kkp5PdTYLcAOq80Q@mail.gmail.com.

~~~

2.
/* Load the WHERE clause for this table. */
- whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-    Anum_pg_publication_rel_prqual,
-    &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prqual,
+ &isnull);
if (!isnull)
- oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+ oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
/* Transform the int2vector column list to a bitmap. */
- columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-   Anum_pg_publication_rel_prattrs,
-   &isnull);
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prattrs,
+ &isnull);
+
+ if (!isnull)
+ oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
+
+ /* Load the prexcept flag for this table. */
+ datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+ Anum_pg_publication_rel_prexcept,
+ &isnull);
if (!isnull)
- oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+ oldexcept = DatumGetBool(datum);

Use consistent spacing. Either do or don't (I prefer don't) put a
blank line between the pairs of "datum =" and "if (!isnull)". Avoid
having a mixture.

======
src/bin/psql/describe.c

addFooterToPublicationOrTableDesc:

3.
+/*
+ * If is_tbl_desc is true add footer to table description else add footer to
+ * publication description.
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf, const char *footermsg,
+   bool as_schema, printTableContent *const cont,
+   bool is_tbl_desc)

3a.
Since you are changing this anyway, I think it would be better to keep
those boolean params together (at the end).

~

3b.
It seems a bit mixed up calling this addFooterToPublicationOrTableDesc
but having the variable 'is_tbl_desc', because it seems more natural
to me to read left to right, so the logical order of everything here
should be pub desc then table desc. In other words, use boolean
'is_pub_desc' instead of 'is_tbl_desc'. Also, I think that 'as_schema'
thing is kind of a *subset* of the publication description, so it
makes more sense for that to come last too.

e.g.
CURRENT
addFooterToPublicationOrTableDesc(buf, footermsg, as_schema, cont, is_tbl_desc)
SUGGESTION
addFooterToPublicationOrTableDesc(buf, cont, footermsg, is_pub_desc, as_schema)

~

3c
While you are changing things, maybe also consider changing that
'as_schema' name because I did not understand what "as" means. Perhaps
rename like 'pub_schemas', or 'only_show_schemas' or something better
(???).

~~~

4.
+ PGresult   *res;
+ int count = 0;
+ int i = 0;
+ int col = is_tbl_desc ? 0 : 1;
+
+ res = PSQLexec(buf->data);
+ if (!res)
+ return false;
+ else
+ count = PQntuples(res);
+

4a.
Assignment count = 0 is redundant.

~

4b.
Remove the 'i' declaration here. Declare it in the "for" loop later.

~

4c.
The "else" is not required. If 'res' was not good, you already returned.

~~~

5.
+ if (as_schema)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
+ else
+ {
+ if (is_tbl_desc)
+ printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, col));
+ else
+ printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
+   PQgetvalue(res, i, col));

This function is basically either (a) a footer for a table description
or (b) a footer for a publication description. And that all hinges on
the boolean 'is_tbl_desc'. Therefore, it seems more natural for the
main condition to be "if (is_tbl_desc)" here.

This turned everything inside out. PSA: a top-up patch to show a way
to do this. Perhaps my implementation is a bit verbose, but OTOH it
seems easier to understand. Anyway, see what you think...

+ 1

Included these changes in the latest patch [2]/messages/by-id/CANhcyEUEMWSkTfGc7Q3B+UiOzSiOmOGLgK-+C5DXwtCGOnDBhg@mail.gmail.com.

6.
+ /*---------------------------------------------------
+ * Publication/ table description columns:
+ * [0]: schema name (nspname)
+ * [col]: table name (relname) / publication name (pubname)
+ * [col + 1]: row filter expression (prqual), may be NULL
+ * [col + 2]: column list (comma-separated), may be NULL
+ * [col + 3]: except flag ("t" if EXCEPT, else "f")
+ *---------------------------------------------------

I've modified this comment slightly so I could understand it better.
See if you agree.

For me that's equal. lets see what other people think

For now I have used the version shared by Peter. I felt it was more descriptive.

[1]: /messages/by-id/CANhcyEXHiCbk2q8=bq3boQDyc8ac9fjgK-kkp5PdTYLcAOq80Q@mail.gmail.com
[2]: /messages/by-id/CANhcyEUEMWSkTfGc7Q3B+UiOzSiOmOGLgK-+C5DXwtCGOnDBhg@mail.gmail.com

Thanks,
Shlok Kyal

#117Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#115)
Re: Skipping schema changes in publication

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

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

#118Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#117)
3 attachment(s)
Re: Skipping schema changes in publication

On Thu, 21 Aug 2025 at 05:33, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

I have fixed it and attached the updated patches

Thanks,
Shlok Kyal

Attachments:

v22-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v22-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 66c664bae1c15beb23860d3d8f74986a99bb27a1 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v22 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 215 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 858 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da8a7882580..e96a55fecf9 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 0ac29928f17..7e9e27aba4e 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2299,10 +2299,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..ca8f6dc9b9f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index b911efcf9cb..af1b8c9ed67 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 06f6f45526b..b6d546be291 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,6 +204,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +283,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +310,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +371,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +395,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -525,7 +532,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -921,52 +928,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1028,7 +1033,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1148,7 +1153,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1244,6 +1249,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1271,7 +1297,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1289,6 +1318,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1327,7 +1429,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1428,6 +1531,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1478,7 +1582,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1601,6 +1706,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1813,6 +1932,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1885,6 +2005,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1960,8 +2081,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 082a3575d62..c1d8aaa9901 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a4010296af..49bac034d17 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10702,7 +10703,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10722,12 +10723,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10765,6 +10767,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10840,6 +10843,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10852,6 +10874,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10878,6 +10902,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 80540c017bd..b9d676d1f18 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 6fe268a8eec..ebcf6d3bd32 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index fc7a6639163..105031a5cbc 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4597,8 +4599,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4764,6 +4792,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4775,8 +4804,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4787,6 +4824,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4802,6 +4840,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4813,6 +4852,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4826,7 +4866,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4867,6 +4911,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11729,6 +11776,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20095,6 +20145,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..c178edb4e05 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 2d02456664b..47a450c820c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -442,6 +444,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1714,6 +1727,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e7a2d64f741..7e4e589919b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3354,6 +3354,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7a06af48842..f3fe4ab30f8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index f07af7f71d3..5efdcf56347 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,11 +2269,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3588,6 +3593,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 74009a92f3f..49afeb77622 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1964,6 +2028,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1982,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1999,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2037,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 1366b11bba0..6240cd97ce3 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,17 +1238,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1243,6 +1269,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v22-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v22-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From bf18e0da0f0fcd453dde16627ddcd2da12282315 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v22 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 190 +++++++++++++
 19 files changed, 897 insertions(+), 215 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e96a55fecf9..c14077caa68 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 7e9e27aba4e..930cae7034f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index af1b8c9ed67..d21b3ff48e1 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1282,6 +1306,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,11 +1331,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1322,8 +1365,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1401,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1416,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b6d546be291..db824d25d74 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -204,7 +204,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -358,8 +357,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -381,6 +380,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -404,7 +404,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -494,8 +495,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1452,6 +1459,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1465,23 +1473,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1508,13 +1521,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 49bac034d17..cebd7c2a3c4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4450,6 +4450,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10759,14 +10764,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10782,7 +10788,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10790,7 +10796,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10800,8 +10806,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10809,25 +10816,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19695,6 +19704,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..68ff559e80c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -821,10 +821,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -838,6 +846,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -888,7 +899,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +913,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +933,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +980,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1045,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1178,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b9d676d1f18..1cf90f1875d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,19 +1141,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1187,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1197,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2109,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2159,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 105031a5cbc..a57ba69f748 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4865,24 +4865,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4908,10 +4891,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4991,7 +4993,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c178edb4e05..5e2aa1b0cf0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f3fe4ab30f8..a1cd247e7f5 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3019,16 +3104,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3038,35 +3134,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3087,34 +3215,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6491,49 +6593,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6704,6 +6763,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,11 +6777,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6728,8 +6790,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6739,14 +6801,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 5efdcf56347..dd560c9ba8c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2272,6 +2272,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3601,7 +3603,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 49afeb77622..69404c6aa1f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,94 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6240cd97ce3..bf64e8a3ce1 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,61 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..79e63c0f449
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,190 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED, bgen int GENERATED ALWAYS AS (2) STORED);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+	CREATE TABLE tab6 (agen int, bgen int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'check initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'check initial sync for EXCEPT (column-list) publication');
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column are is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v22-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v22-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From e1f54391239035fa2e2b5f1a840a3ad34d9c1d6c Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v22 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 803c26ab216..06f6f45526b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -49,6 +49,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -91,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1187,6 +1196,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1501,6 +1603,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..4a4010296af 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10850,6 +10850,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10896,6 +10898,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b10f2313f3..f07af7f71d3 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,7 +2266,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 53268059142..74009a92f3f 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index deddf0da844..1366b11bba0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
-- 
2.34.1

#119Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Shlok Kyal (#118)
3 attachment(s)
Re: Skipping schema changes in publication

On Mon, 25 Aug 2025 at 13:38, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 21 Aug 2025 at 05:33, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

I have fixed it and attached the updated patches

The patches were not applying on HEAD and needed a Rebase. Here is the
rebased patches

Thanks,
Shlok Kyal

Attachments:

v23-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v23-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 51e570328523602f0b52ac0c8d00fc4ae30c29f3 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v23 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 3de5687461c..5a7247b6324 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -48,6 +48,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -90,12 +99,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1186,6 +1195,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1500,6 +1602,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..ce6e0be8e91 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10851,6 +10851,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10897,6 +10899,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6b20a4404b2..01332f4e4df 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2266,7 +2266,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..af220b02788 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 895ca87a0df..fcfc8a9b485 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 3f423061395..48fa53fc6c4 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
-- 
2.34.1

v23-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v23-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From ece7b98718e960c21ab57fc3952ca41589250a1f Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v23 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 215 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 858 insertions(+), 140 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e9095bedf21..ae38619e219 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 9ccd5ec5006..0d106eb95a7 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2299,10 +2299,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..ca8f6dc9b9f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index b911efcf9cb..af1b8c9ed67 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 5a7247b6324..585ea504ffa 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -203,6 +203,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -277,7 +282,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -304,7 +309,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -364,7 +370,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -388,7 +394,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -524,7 +531,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -920,52 +927,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1027,7 +1032,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1147,7 +1152,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1243,6 +1248,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1270,7 +1296,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1288,6 +1317,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1326,7 +1428,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1427,6 +1530,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1477,7 +1581,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1600,6 +1705,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1812,6 +1931,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1884,6 +2004,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
@@ -1959,8 +2080,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 {
 	ListCell   *lc;
 
-	Assert(!stmt || !stmt->for_all_tables);
-
 	foreach(lc, rels)
 	{
 		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 082a3575d62..c1d8aaa9901 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8623,7 +8623,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18818,7 +18818,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ce6e0be8e91..2512889cb9e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10703,7 +10704,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10723,12 +10724,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10766,6 +10768,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10841,6 +10844,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10853,6 +10875,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10879,6 +10903,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 80540c017bd..b9d676d1f18 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 6fe268a8eec..ebcf6d3bd32 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bea793456f9..c634f0aa676 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4597,8 +4599,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4764,6 +4792,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4775,8 +4804,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4787,6 +4824,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4802,6 +4840,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4813,6 +4852,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4826,7 +4866,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4867,6 +4911,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11743,6 +11790,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20109,6 +20159,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bcc94ff07cc..79c2bdd4c82 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 2d02456664b..47a450c820c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -442,6 +444,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1714,6 +1727,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e7a2d64f741..7e4e589919b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3354,6 +3354,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4aa793d7de7..2774548a2c8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 01332f4e4df..5db8812c0d9 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2269,11 +2269,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3589,6 +3594,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af220b02788..f2cc9784456 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index fcfc8a9b485..9bfc43344d5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1964,6 +2028,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1982,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1999,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2037,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 48fa53fc6c4..fbe19a6959b 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,17 +1238,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1243,6 +1269,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..e8a117f3421 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v23-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v23-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 8b44330d45346b5728fccc3629cd4dbea75b6f41 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v23 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 190 +++++++++++++
 19 files changed, 897 insertions(+), 215 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ae38619e219..13a4ec3637c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 0d106eb95a7..bf0c4a5e11f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index af1b8c9ed67..d21b3ff48e1 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1282,6 +1306,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,11 +1331,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1322,8 +1365,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1401,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1416,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 585ea504ffa..facb2401d56 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -203,7 +203,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -357,8 +356,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -380,6 +379,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -403,7 +403,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -493,8 +494,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1451,6 +1458,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1464,23 +1472,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1507,13 +1520,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2512889cb9e..8005abe0780 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4451,6 +4451,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10760,14 +10765,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10783,7 +10789,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10791,7 +10797,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10801,8 +10807,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10810,25 +10817,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19696,6 +19705,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..68ff559e80c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -821,10 +821,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -838,6 +846,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -888,7 +899,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +913,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +933,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +980,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1045,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1178,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b9d676d1f18..1cf90f1875d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,19 +1141,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1187,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1197,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2109,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2159,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c634f0aa676..3f39b09511b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4865,24 +4865,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4908,10 +4891,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -4991,7 +4993,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 79c2bdd4c82..cd86e7ed14a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2774548a2c8..9f5d5b04fca 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3019,16 +3104,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3038,35 +3134,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3087,34 +3215,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6491,49 +6593,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6704,6 +6763,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,11 +6777,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6728,8 +6790,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6739,14 +6801,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 5db8812c0d9..56002cf1cc9 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2272,6 +2272,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny))
+		COMPLETE_WITH("EXCEPT (");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -3602,7 +3604,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 9bfc43344d5..9b167303e54 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2125,6 +2125,94 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index fbe19a6959b..a69adf88eb7 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,61 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index e8a117f3421..e010de1e1d0 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -43,6 +43,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..79e63c0f449
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,190 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED, bgen int GENERATED ALWAYS AS (2) STORED);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+	CREATE TABLE tab6 (agen int, bgen int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'check initial sync for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'check initial sync for EXCEPT (column-list) publication');
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column are is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#120vignesh C
vignesh21@gmail.com
In reply to: Shlok Kyal (#119)
Re: Skipping schema changes in publication

On Fri, 5 Sept 2025 at 11:57, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 25 Aug 2025 at 13:38, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 21 Aug 2025 at 05:33, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

I have fixed it and attached the updated patches

The patches were not applying on HEAD and needed a Rebase. Here is the
rebased patches

Consider the following scenario:
create table t1(c1 int, c2 int);
create publication pub1 for table t1 except (c1, c2);

In this case, the publication is created in such a way that no columns
are included, so effectively no data will be replicated to the
subscriber.
However, when attempting an UPDATE, the following error occurs:
postgres=# update t1 set c1 = 2;
ERROR: cannot update table "t1" because it does not have a replica
identity and publishes updates
HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.

Is this behavior expected?

Regards,
Vignesh

#121vignesh C
vignesh21@gmail.com
In reply to: Shlok Kyal (#119)
Re: Skipping schema changes in publication

On Fri, 5 Sept 2025 at 11:57, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 25 Aug 2025 at 13:38, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 21 Aug 2025 at 05:33, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

I have fixed it and attached the updated patches

The patches were not applying on HEAD and needed a Rebase. Here is the
rebased patches

Few comments:
1) Currently from pg_publication_tables it is not clear if it is
replicating column list or replicating exclude column, can we indicate
if it is exclude or not:
create publication pub1 for table t1(c1);
create publication pub2 for table t1 except ( c1);

postgres=# select * from pg_publication_tables;
pubname | schemaname | tablename | attnames | rowfilter
---------+------------+-----------+----------+-----------
pub1 | public | t1 | {c1} |
pub2 | public | t1 | {c2} |
(2 rows)

2) Tab completion is not correct in this case:
postgres=# alter publication pub3 add table t2 EXCEPT (
, WHERE (

3) tab6 is not used anywhere, it can be removed:
+       CREATE TABLE tab5 (a int, b int, c int);
+       CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED,
bgen int GENERATED ALWAYS AS (2) STORED);
+       INSERT INTO tab1 VALUES (1, 2, 3);
4) both these tests are using same message:
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+       'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');

we can include table name here to differentiate the test that will
help in identifying test failure easily

5) /newly added column are is replicated/ should be "newly added
column is replicated"
is($result, qq(|||10), 'newly added column are is replicated');

Regards,
Vignesh

#122Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: vignesh C (#120)
Re: Skipping schema changes in publication

On Thu, 25 Sept 2025 at 14:18, vignesh C <vignesh21@gmail.com> wrote:

On Fri, 5 Sept 2025 at 11:57, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 25 Aug 2025 at 13:38, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 21 Aug 2025 at 05:33, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

I have fixed it and attached the updated patches

The patches were not applying on HEAD and needed a Rebase. Here is the
rebased patches

Consider the following scenario:
create table t1(c1 int, c2 int);
create publication pub1 for table t1 except (c1, c2);

In this case, the publication is created in such a way that no columns
are included, so effectively no data will be replicated to the
subscriber.
However, when attempting an UPDATE, the following error occurs:
postgres=# update t1 set c1 = 2;
ERROR: cannot update table "t1" because it does not have a replica
identity and publishes updates
HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.

Is this behavior expected?

Hi Vignesh,

I think this behaviour is same as other similar cases like:

1. publication on empty table:
CREATE TABLE t1();
CREATE PUBLICATION pub1 FOR TABLE t1;

postgres=# DELETE FROM t1;
ERROR: cannot delete from table "t1" because it does not have a
replica identity and publishes deletes
HINT: To enable deleting from the table, set REPLICA IDENTITY using
ALTER TABLE.

2. All the columns in a table is a generated column:
CREATE TABLE t2(a int GENERATED ALWAYS AS (2*2) STORED);
CREATE PUBLICATION pub2 FOR TABLE t2 WITH (publish_generated_columns='none');

In this case since "publish_generated_columns=none", should not
publish changes for table t2. But we get following:
postgres=# DELETE FROM t2;
ERROR: cannot delete from table "t2" because it does not have a
replica identity and publishes deletes
HINT: To enable deleting from the table, set REPLICA IDENTITY using
ALTER TABLE.

In above cases as well no columns are published but we have the similar error.
Given these behaviours in HEAD I think it is okay for EXCEPT
column_list to have the similar behaviour when all columns are
excluded. Thought?

Thanks,
Shlok Kyal

#123Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: vignesh C (#121)
3 attachment(s)
Re: Skipping schema changes in publication

On Thu, 25 Sept 2025 at 16:39, vignesh C <vignesh21@gmail.com> wrote:

On Fri, 5 Sept 2025 at 11:57, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 25 Aug 2025 at 13:38, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 21 Aug 2025 at 05:33, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I reviewed your latest v20-0003 patch and have no more comments at
this time; I only found one trivial typo.

======
src/bin/psql/describe.c

1.
+ /*
+ * Footers entries for a publication description or a table
+ * description
+ */

Typo. /Footers entries/Footer entries/

I have fixed it and attached the updated patches

The patches were not applying on HEAD and needed a Rebase. Here is the
rebased patches

Few comments:
1) Currently from pg_publication_tables it is not clear if it is
replicating column list or replicating exclude column, can we indicate
if it is exclude or not:
create publication pub1 for table t1(c1);
create publication pub2 for table t1 except ( c1);

postgres=# select * from pg_publication_tables;
pubname | schemaname | tablename | attnames | rowfilter
---------+------------+-----------+----------+-----------
pub1 | public | t1 | {c1} |
pub2 | public | t1 | {c2} |
(2 rows)

2) Tab completion is not correct in this case:
postgres=# alter publication pub3 add table t2 EXCEPT (
, WHERE (

3) tab6 is not used anywhere, it can be removed:
+       CREATE TABLE tab5 (a int, b int, c int);
+       CREATE TABLE tab6 (agen int GENERATED ALWAYS AS (1) STORED,
bgen int GENERATED ALWAYS AS (2) STORED);
+       INSERT INTO tab1 VALUES (1, 2, 3);
4) both these tests are using same message:
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+       'check incremental insert for EXCEPT (column-list) publication');
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||), 'check incremental insert for EXCEPT (column-list) publication');

we can include table name here to differentiate the test that will
help in identifying test failure easily

5) /newly added column are is replicated/ should be "newly added
column is replicated"
is($result, qq(|||10), 'newly added column are is replicated');

Hi Vignesh,

Thanks for reviewing the patch.
I have addressed the comments and attached the updated version.

Thanks,
Shlok Kyal

Attachments:

v24-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v24-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From c885989817c2c53328b992ad5220c01b14ea9a77 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Sun, 3 Aug 2025 21:01:12 +0530
Subject: [PATCH v24 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 doc/src/sgml/system-views.sgml                |  10 +
 src/backend/catalog/pg_publication.c          |  86 +++++-
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     | 130 +++++++--
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_collist.pl       | 193 +++++++++++++
 23 files changed, 956 insertions(+), 245 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ae38619e219..13a4ec3637c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6573,7 +6573,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 0d106eb95a7..bf0c4a5e11f 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1372,10 +1372,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1389,8 +1389,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1402,6 +1405,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1423,11 +1434,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1472,18 +1486,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1497,6 +1514,7 @@ Publications:
  postgres | f          | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1517,23 +1535,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1545,11 +1581,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1558,6 +1604,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1654,6 +1707,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 38b4657378a..f79ef789d93 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -260,6 +260,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, department
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index bddff9ca0cc..0691b102840 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -28,7 +28,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -92,17 +92,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -347,10 +354,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -373,6 +382,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -494,6 +513,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
 <programlisting>
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
+
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 4187191ea74..75c3c9e5871 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2632,6 +2632,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Expression for the table's publication qualifying condition
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>exceptcol</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if a column list with <literal>EXCEPT</literal> clause is specified
+       for the table in the publication.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index af1b8c9ed67..0f6d42ccd5c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -263,14 +263,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -296,6 +301,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -657,10 +672,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -683,6 +700,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -787,8 +807,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -828,10 +850,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1155,7 +1179,7 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 Datum
 pg_get_publication_tables(PG_FUNCTION_ARGS)
 {
-#define NUM_PUBLICATION_TABLES_ELEM	4
+#define NUM_PUBLICATION_TABLES_ELEM	5
 	FuncCallContext *funcctx;
 	List	   *table_infos = NIL;
 
@@ -1261,6 +1285,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						   INT2VECTOROID, -1, 0);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 4, "qual",
 						   PG_NODE_TREEOID, -1, 0);
+		TupleDescInitEntry(tupdesc, (AttrNumber) 5, "exceptcol",
+						   BOOLOID, -1, 0);
 
 		funcctx->tuple_desc = BlessTupleDesc(tupdesc);
 		funcctx->user_fctx = table_infos;
@@ -1282,6 +1308,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1306,11 +1333,36 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+				{
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+
+					if (except_columns)
+						values[4] = BoolGetDatum(true);
+					else
+						values[4] = BoolGetDatum(false);
+				}
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1322,8 +1374,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1354,6 +1410,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1362,6 +1425,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c77fa0234bb..e2afcdb884d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -388,7 +388,8 @@ CREATE VIEW pg_publication_tables AS
           WHERE a.attrelid = GPT.relid AND
                 a.attnum = ANY(GPT.attrs)
         ) AS attnames,
-        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
+        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter,
+        gpt.exceptcol AS exceptcol
     FROM pg_publication P,
          LATERAL pg_get_publication_tables(P.pubname) GPT,
          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 7f847b116bf..be1efd9bdd0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -203,7 +203,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -357,8 +356,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -380,6 +379,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -403,7 +403,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -493,8 +494,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1451,6 +1458,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1464,23 +1472,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1507,13 +1520,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2512889cb9e..8005abe0780 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -527,7 +527,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4451,6 +4451,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10760,14 +10765,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10783,7 +10789,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10791,7 +10797,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10801,8 +10807,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10810,25 +10817,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19696,6 +19705,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index e6da4028d39..0263384645a 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -821,10 +821,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -838,6 +846,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -888,7 +899,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -902,7 +913,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -912,7 +933,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -959,6 +980,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -1021,7 +1045,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1153,11 +1178,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b9d676d1f18..1cf90f1875d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1068,12 +1078,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1122,19 +1141,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1146,7 +1187,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1156,9 +1197,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2059,6 +2109,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2108,6 +2159,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 5b8cb277026..82af91b0bcf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4919,24 +4919,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4962,10 +4945,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -5045,7 +5047,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 79c2bdd4c82..cd86e7ed14a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -689,6 +689,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2774548a2c8..9f5d5b04fca 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3019,16 +3104,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3038,35 +3134,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3087,34 +3215,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6491,49 +6593,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6704,6 +6763,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6712,11 +6777,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6728,8 +6790,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6739,14 +6801,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 2ca28c071db..385c6d63616 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2282,6 +2282,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT (");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT"))
+		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -2302,10 +2306,13 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "WHERE", "("))
 		COMPLETE_WITH_ATTR(prev3_wd);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT", "("))
+		COMPLETE_WITH_ATTR(prev3_wd);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 !TailMatches("WHERE", "(*)"))
+			 !TailMatches("WHERE", "(*)") && !TailMatches("EXCEPT", "("))
 		COMPLETE_WITH(",", "WHERE (");
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 !ends_with(prev_wd, '('))
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
@@ -3604,7 +3611,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 01eba3b5a19..b35f173ae1d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12290,9 +12290,9 @@
   proname => 'pg_get_publication_tables', prorows => '1000',
   provariadic => 'text', proretset => 't', provolatile => 's',
   prorettype => 'record', proargtypes => '_text',
-  proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
-  proargmodes => '{v,o,o,o,o}',
-  proargnames => '{pubname,pubid,relid,attrs,qual}',
+  proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree,bool}',
+  proargmodes => '{v,o,o,o,o,o}',
+  proargnames => '{pubname,pubid,relid,attrs,qual,exceptcol}',
   prosrc => 'pg_get_publication_tables' },
 { oid => '6121',
   descr => 'returns whether a relation can be part of a publication',
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 022467fb45c..2a1dc48ccb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -181,7 +181,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -191,6 +192,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 9bfc43344d5..d9f2dca1fd0 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1818,52 +1818,52 @@ CREATE TABLE sch2.tbl1_part1 PARTITION OF sch1.tbl1 FOR VALUES FROM (1) to (10);
 -- Schema publication that does not include the schema that has the parent table
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
 -- Table publication that does not include the parent table
 CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 -- Table publication that includes both the parent table and the child table
 ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename | attnames | rowfilter 
----------+------------+-----------+----------+-----------
- pub     | sch1       | tbl1      | {a}      | 
+ pubname | schemaname | tablename | attnames | rowfilter | exceptcol 
+---------+------------+-----------+----------+-----------+-----------
+ pub     | sch1       | tbl1      | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
 -- Schema publication that does not include the schema that has the parent table
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
 -- Table publication that does not include the parent table
 CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 -- Table publication that includes both the parent table and the child table
 ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
@@ -1876,9 +1876,9 @@ CREATE TABLE sch1.tbl1_part3 (a int) PARTITION BY RANGE(a);
 ALTER TABLE sch1.tbl1 ATTACH PARTITION sch1.tbl1_part3 FOR VALUES FROM (20) to (30);
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename | attnames | rowfilter 
----------+------------+-----------+----------+-----------
- pub     | sch1       | tbl1      | {a}      | 
+ pubname | schemaname | tablename | attnames | rowfilter | exceptcol 
+---------+------------+-----------+----------+-----------+-----------
+ pub     | sch1       | tbl1      | {a}      |           | f
 (1 row)
 
 RESET client_min_messages;
@@ -2125,6 +2125,94 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter | exceptcol 
+----------------+------------+------------------+-----------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} |           | f
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     |           | t
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                           Publication testpub_except
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 35e8aad7701..ca645fc353a 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1468,9 +1468,10 @@ pg_publication_tables| SELECT p.pubname,
     ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg
            FROM pg_attribute a
           WHERE ((a.attrelid = gpt.relid) AND (a.attnum = ANY ((gpt.attrs)::smallint[])))) AS attnames,
-    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter
+    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter,
+    gpt.exceptcol
    FROM pg_publication p,
-    LATERAL pg_get_publication_tables(VARIADIC ARRAY[(p.pubname)::text]) gpt(pubid, relid, attrs, qual),
+    LATERAL pg_get_publication_tables(VARIADIC ARRAY[(p.pubname)::text]) gpt(pubid, relid, attrs, qual, exceptcol),
     (pg_class c
      JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
   WHERE (c.oid = gpt.relid);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index fbe19a6959b..a69adf88eb7 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1318,6 +1318,61 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index c4c1efd27bc..b51c620cb83 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_rep_changes_except_table.pl',
+      't/037_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_collist.pl b/src/test/subscription/t/037_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..3dfd266bc3d
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_collist.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'Verify initial sync of tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'Verify initial sync of sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'Verify incremental inserts on tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||),
+	'Verify incremental inserts on sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v24-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v24-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From e1de6ef25a857c4a0b940eb0f097da517cacf2af Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Jun 2025 11:41:18 +0530
Subject: [PATCH v24 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 +++++--
 src/backend/commands/publicationcmds.c    | 116 +++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 +++++++++++
 7 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d5ea383e8bc..178f39d9575 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
    or <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
@@ -230,6 +244,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index f4fc17acbe1..b9daff74a3c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -48,6 +48,15 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -90,12 +99,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1186,6 +1195,99 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1500,6 +1602,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..ce6e0be8e91 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10851,6 +10851,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10897,6 +10899,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6176741d20b..80d0d26a6ed 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2276,7 +2276,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f1706df58fd..a383bcbe1b3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4305,6 +4305,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 895ca87a0df..fcfc8a9b485 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1923,6 +1923,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 3f423061395..48fa53fc6c4 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1222,6 +1222,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
-- 
2.34.1

v24-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v24-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 1f9fbbdc3f8ccd34d6e312a46ee00dba4e0d4d39 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 22 Jul 2025 00:33:55 +0530
Subject: [PATCH v24 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE t1,t2;
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE t1,t2;

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  97 +++++---
 src/backend/commands/publicationcmds.c        | 213 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  89 +++++++-
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/036_rep_changes_except_table.pl         | 186 +++++++++++++++
 25 files changed, 859 insertions(+), 138 deletions(-)
 create mode 100644 src/test/subscription/t/036_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e9095bedf21..ae38619e219 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6568,6 +6568,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 9ccd5ec5006..0d106eb95a7 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2299,10 +2299,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 178f39d9575..38b4657378a 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -129,7 +136,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -237,6 +245,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT users, departments;
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..bddff9ca0cc 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
 
@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -125,7 +129,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -161,6 +167,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -442,6 +478,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..ca8f6dc9b9f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index b911efcf9cb..af1b8c9ed67 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -351,7 +351,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -363,32 +364,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -463,6 +476,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -479,6 +503,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -746,9 +772,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -762,7 +788,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -771,13 +798,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES
  * should use GetAllTablesPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -802,8 +830,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -861,13 +892,19 @@ GetAllTablesPublications(void)
  * root partitioned tables.
  */
 List *
-GetAllTablesPublicationRelations(bool pubviaroot)
+GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
@@ -884,7 +921,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -905,7 +943,8 @@ GetAllTablesPublicationRelations(bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1160,7 +1199,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot);
+				pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->oid,
+																   pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
@@ -1169,7 +1209,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b9daff74a3c..7f847b116bf 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -203,6 +203,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -277,7 +282,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -304,7 +309,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -364,7 +370,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -388,7 +394,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -524,7 +531,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -920,52 +927,50 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
-	{
-		/* Invalidate relcache so that publication info is rebuilt. */
-		CacheInvalidateRelcacheAll();
-	}
-	else
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1027,7 +1032,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1147,7 +1152,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1243,6 +1248,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1270,7 +1296,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1288,6 +1317,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1326,7 +1428,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1427,6 +1530,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1477,7 +1581,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1600,6 +1705,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1812,6 +1931,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1884,6 +2004,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fc89352b661..ed358d72a7d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8627,7 +8627,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18822,7 +18822,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ce6e0be8e91..2512889cb9e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -446,7 +446,7 @@ 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 pub_obj_list
+				drop_option_list pub_obj_list except_pub_obj_list
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -584,6 +584,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 <publicationobjectspec> ExceptPublicationObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -10703,7 +10704,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * CREATE PUBLICATION name [WITH options]
  *
- * CREATE PUBLICATION FOR ALL TABLES [WITH options]
+ * CREATE PUBLICATION FOR ALL TABLES [EXCEPT [TABLE] table [, ...]] [WITH options]
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
  *
@@ -10723,12 +10724,13 @@ CreatePublicationStmt:
 					n->options = $4;
 					$$ = (Node *) n;
 				}
-			| CREATE PUBLICATION name FOR ALL TABLES opt_definition
+			| CREATE PUBLICATION name FOR ALL TABLES except_pub_obj_list opt_definition
 				{
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					n->options = $7;
+					n->options = $8;
+					n->pubobjects = (List *)$7;
 					n->for_all_tables = true;
 					$$ = (Node *) n;
 				}
@@ -10766,6 +10768,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10841,6 +10844,25 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list:	EXCEPT opt_table ExceptPublicationObjSpec
+					{ $$ = list_make1($3); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+			|  /*EMPTY*/								{ $$ = NULL; }
+	;
+
 /*****************************************************************************
  *
  * ALTER PUBLICATION name SET ( options )
@@ -10853,6 +10875,8 @@ pub_obj_list:	PublicationObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] table_name [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10879,6 +10903,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_pub_obj_list
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 80540c017bd..b9d676d1f18 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2065,7 +2065,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2176,22 +2177,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2210,7 +2195,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2225,6 +2211,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2303,6 +2291,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2b798b823ea..6122bd8b6ee 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9fc3671cb35..5b8cb277026 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4651,8 +4653,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 					  qpubname);
 
 	if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
 
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ",");
+				appendPQExpBuffer(query, " ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+	}
+
+	first = true;
 	appendPQExpBufferStr(query, " WITH (publish = '");
 	if (pubinfo->pubinsert)
 	{
@@ -4818,6 +4846,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4829,8 +4858,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4841,6 +4878,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4856,6 +4894,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4867,6 +4906,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4880,7 +4920,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4921,6 +4965,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11797,6 +11844,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20168,6 +20218,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bcc94ff07cc..79c2bdd4c82 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 2d02456664b..47a450c820c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -442,6 +444,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1714,6 +1727,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fc5b9b52f80..2aa8eb074fd 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3432,6 +3432,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub6' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT dump_test.test_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub6 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub7' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE dump_test.test_table, dump_test.test_second_table;',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub7 FOR ALL TABLES EXCEPT TABLE ONLY dump_test.test_table, ONLY dump_test.test_second_table WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4aa793d7de7..2774548a2c8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3039,17 +3039,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6693,8 +6710,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6712,6 +6733,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 80d0d26a6ed..2ca28c071db 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2279,11 +2279,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3591,6 +3596,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 6e074190fd2..022467fb45c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -139,11 +139,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -161,9 +162,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllTablesPublicationRelations(bool pubviaroot);
+extern List *GetAllTablesPublicationRelations(Oid pubid, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -174,7 +176,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a383bcbe1b3..9a775a2b55b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4268,6 +4268,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4276,6 +4277,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4321,6 +4323,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index fcfc8a9b485..9bfc43344d5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                  Publication testpub_foralltables_excepttable
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                 Publication testpub_foralltables_excepttable1
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
                                               Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                              Publication testpub5
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                              Publication testpub6
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_forparted;
@@ -1926,9 +1967,15 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1945,7 +1992,24 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1964,6 +2028,11 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1982,6 +2051,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -1999,6 +2074,12 @@ ALTER PUBLICATION testpub_reset RESET;
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                            Publication testpub_reset
@@ -2037,9 +2118,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 48fa53fc6c4..fbe19a6959b 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE testpub_tbl1, testpub_tbl2;
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT testpub_tbl1;
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE testpub_tbl3;
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE ONLY testpub_tbl3;
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 -- Tests for partitioned tables
 SET client_min_messages = 'ERROR';
@@ -1225,17 +1238,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1, pub_sch1.tbl2;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1243,6 +1269,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1250,6 +1279,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1257,6 +1290,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE pub_sch1.tbl1;
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1273,10 +1310,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 20b4e523d93..c4c1efd27bc 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/036_rep_changes_except_table.pl b/src/test/subscription/t/036_rep_changes_except_table.pl
new file mode 100644
index 00000000000..a9d73fe721d
--- /dev/null
+++ b/src/test/subscription/t/036_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE sch1.tab1"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE sch1.tab1, public.tab1;
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.part1");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.t1;
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE sch1.t1 WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT sch1.part1;
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#124Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#123)
Re: Skipping schema changes in publication

Hi Shlok,

I was looking at the recent v24 changes.

======
GENERAL.

I saw that you modified the system view to add a new flag:

+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>exceptcol</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if a column list with <literal>EXCEPT</literal> clause is specified
+       for the table in the publication.
+      </para></entry>
+     </row>

So output now might look like this:

+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1,
pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  |
rowfilter | exceptcol
+----------------+------------+------------------+-----------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} |           | f
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     |           | t
+(2 rows)

~~~

I think this was done in response to a comment from Vignesh [1]/messages/by-id/CALDaNm32XQDR4qsOhPQeophVbZ8r+ShJSSssoVfdPcwG6joPHQ@mail.gmail.com, but
it did not get implemented in the way that I had imagined. e.g. I
imagined the view might be more like this:

+    pubname     | schemaname |    tablename     | attnames  |
rowfilter | exceptcols
+----------------+------------+------------------+-----------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} |           |
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     |           | {b,c}

I don't know if broadcasting to the user what the unpublished/hidden
columns' names are is very wise (e.g. "{password,internal_notes,
salary}", but OTOH just having a boolean flag saying that "something"
was excluded ddin't seem useful.

~

Furthermore, having a Boolean seemed strangely incompatible with a
normal column list. e.g. Lets say there is a table T1 with cols
c1,c2,c3,c4.

I could publish that as "FOR TABLE T1(c1,c2,c3)"
Or as "FOR TABLE T1 EXCEPT (c4)"

In the v24 implementation, AFAIK, the view will show those as
"attnames = {c1,c2,c3}", and except will be both "f" and "t". It
seemed odd to.

~

Lastly, I think the EXCEPT (col-list) feature was mostly added just
to help users with 100s of columns to write their CREATE PUBLICATION
statement more easily. Since the view already shows all the columns
that will be published. So, I'm kind of -0.5 on this idea of changing
the view to show how they typed their statement.

======
[1]: /messages/by-id/CALDaNm32XQDR4qsOhPQeophVbZ8r+ShJSSssoVfdPcwG6joPHQ@mail.gmail.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#125vignesh C
vignesh21@gmail.com
In reply to: Shlok Kyal (#123)
Re: Skipping schema changes in publication

On Sat, 27 Sept 2025 at 01:20, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

Thanks for reviewing the patch.
I have addressed the comments and attached the updated version.

If all columns are excluded, we do not publish the changes. However,
when a table has no columns, the data is still replicated. Should we
make this behavior consistent?
@@ -1482,6 +1525,13 @@ pgoutput_change(LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
        relentry = get_rel_sync_entry(data, relation);
+       /*
+        * If all columns of a table are present in column list specified with
+        * EXCEPT, skip publishing the changes.
+        */
+       if (relentry->all_cols_excluded)
+               return;

Steps to check the above issue:
-- pub
create table t1();
create table t2(c1 int, c2 int);
create publication pub1 FOR table t1;
create publication pub2 FOR table t2 except(c1, c2);

--sub
create table t1(c1 int);
create table t2(c1 int, c2 int);
create subscription sub1 connection 'dbname=postgres host=localhost
port=5432' publication pub1,pub2;

--pub
postgres=# insert into t1 default values ;
INSERT 0 1
postgres=# insert into t2 default values;
INSERT 0 1

--sub
-- In case of table having no columns, data is replicated
postgres=# select * from t1;
c1
----

(1 row)

-- In case of table having all columns excluded, data is not replicated
postgres=# select * from t2;
c1 | c2
----+----
(0 rows)

Thoughts?

Regards,
Vignesh

#126vignesh C
vignesh21@gmail.com
In reply to: Peter Smith (#124)
Re: Skipping schema changes in publication

On Mon, 29 Sept 2025 at 08:58, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok,

I was looking at the recent v24 changes.

======
GENERAL.

I saw that you modified the system view to add a new flag:

+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>exceptcol</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if a column list with <literal>EXCEPT</literal> clause is specified
+       for the table in the publication.
+      </para></entry>
+     </row>

So output now might look like this:

+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1,
pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  |
rowfilter | exceptcol
+----------------+------------+------------------+-----------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} |           | f
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     |           | t
+(2 rows)

~~~

I think this was done in response to a comment from Vignesh [1], but
it did not get implemented in the way that I had imagined. e.g. I
imagined the view might be more like this:

+    pubname     | schemaname |    tablename     | attnames  |
rowfilter | exceptcols
+----------------+------------+------------------+-----------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} |           |
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     |           | {b,c}

I don't know if broadcasting to the user what the unpublished/hidden
columns' names are is very wise (e.g. "{password,internal_notes,
salary}", but OTOH just having a boolean flag saying that "something"
was excluded ddin't seem useful.

~

Furthermore, having a Boolean seemed strangely incompatible with a
normal column list. e.g. Lets say there is a table T1 with cols
c1,c2,c3,c4.

I could publish that as "FOR TABLE T1(c1,c2,c3)"
Or as "FOR TABLE T1 EXCEPT (c4)"

In the v24 implementation, AFAIK, the view will show those as
"attnames = {c1,c2,c3}", and except will be both "f" and "t". It
seemed odd to.

~

Lastly, I think the EXCEPT (col-list) feature was mostly added just
to help users with 100s of columns to write their CREATE PUBLICATION
statement more easily. Since the view already shows all the columns
that will be published. So, I'm kind of -0.5 on this idea of changing
the view to show how they typed their statement.

On further consideration, I’m ok with removing this column to avoid
potential confusion.

Regards,
Vignesh

#127Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#123)
Re: Skipping schema changes in publication

Hi Vignesh,

I had a look at patch v24-0001.

FYI -- a rebase is needed

[postgres@CentOS7-x64 oss_postgres_misc]$ git apply
../patches_misc/v24-0001-Add-RESET-clause-to-Alter-Publication-which-will.patch
error: patch failed: doc/src/sgml/ref/alter_publication.sgml:69
error: doc/src/sgml/ref/alter_publication.sgml: patch does not apply

Here are some other review comments:

======

1.
There seems to be some basic omission of the ALTER PUBLICATION in that
it does not support "ALL TABLES" as a publication_object.

Therefore, if you have:
CREATE PUBLICATION mypub FOR ALL TABLES;

and then you do:
ALTER PUBLICATION mypub RESET;

There seems to be no way to restore mpub to be an ALL TABLES publication again!

~~~

I think if you are going to implement a RESET, then you also need a
way to get back to what you had before you did the reset. So you'll
also need to implement the ALTER PUBLICATION mypub SET ALL TABLES;

IMO, supporting "SET ALL TABLES" should be your new 0001 patch
because AFAIK, this situation already exists if the user had created
an "empty" publication:
CREATE PUBLICATION myemptypub;

======
doc/src/sgml/ref/alter_publication.sgml

2.
Probably need to mention ALL SEQUENCES now too?

======
src/backend/commands/publicationcmds.c

3.
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+

Is it better to put all these in the catalog/pg_publication.h where
the catalog was defined?

~~~

AlterPublicationReset:

4.
+ /* Set ALL TABLES flag to false */
+ if (pubform->puballtables)
+ {
+ values[Anum_pg_publication_puballtables - 1] =
BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+ replaces[Anum_pg_publication_puballtables - 1] = true;
+ CacheInvalidateRelcacheAll();
+ }

Why not just do this anyway without the condition?

======
src/backend/parser/gram.y

6.
It would be nicer if all these grammar productions were coded in the
same order as the comment above them.

======
src/include/nodes/parsenodes.h

7.
AP_AddObjects, /* add objects to publication */
AP_DropObjects, /* remove objects from publication */
AP_SetObjects, /* set list of objects */
+ AP_ResetPublication, /* reset the publication */
} AlterPublicationAction;

It is already called "AlterPublicationAction", so maybe the enum value
only needs to be AP_Reset instead of AP_ResetPublication.

======
src/test/regress/expected/publication.out

8.
Expected output all needs rebasing now that there is a new "All
sequences" column.

======
src/test/regress/sql/publication.sql

9.
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+

I felt the ADD TABLE should be after the comment.

And ditto for all the other test cases -- should be that same pattern too.

# comment about test
ALTER .. do something
\dRp+ pub
ALTER .. RESET
\dRp+ pub

~~~

10.
+-- Verify that associated schemas are reomved from the publication after RESET

typo: /reomved/removed/

~~~

11.
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+

Perhaps this should be the first test?

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

#128Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#123)
Re: Skipping schema changes in publication

Hi Vignesh

Here are some review comments for the patch v24-0002.

These comments are just for the SGML docs. The patch needs a rebase so
I was unable to review the code.

======
Commit message

1.
A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

~

/to maintain/to flag/

======
doc/src/sgml/logical-replication.sgml

2.
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>

Those "FOR ALL TABLES" etc are missing SGML markup.

======
doc/src/sgml/ref/alter_publication.sgml

3.
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable
class="parameter">exception_object</replaceable> [, ... ] ]

and

+
+<phrase>where <replaceable
class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+

It is not clear from the syntax which of these is possible.

... ADD ALL TABLES EXCEPT TABLE t1,t2,t3
... ADD ALL TABLES EXCEPT TABLE t1, TABLE t2, TABLES t3

IMO it is best put the "[TABLE]" within the exception_object:
[ TABLE ] [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

Then both are possible, which is consistent with how "FOR TABLE" syntax works.

Furthermore, you might want later to say EXCLUDE SEQUENCE, so doing it
this way makes that possible.

~~~

4.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,

This wording seems a bit awkward. How are re-phrasing like:

SUGGESTION
Adding or excluding a table from a publication requires ownership of that table.

~~~

5.
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.

typo: /donot/does not/

======
doc/src/sgml/ref/create_publication.sgml

6.
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable
class="parameter">exception_object</replaceable> [, ... ] ]
       | FOR <replaceable
class="parameter">publication_object</replaceable> [, ... ] ]
     [ WITH ( <replaceable
class="parameter">publication_parameter</replaceable> [= <replaceable
class="parameter">value</replaceable>] [, ... ] ) ]

@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable
class="parameter">name</replaceable>

     TABLE [ ONLY ] <replaceable
class="parameter">table_name</replaceable> [ * ] [ ( <replaceable
class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE (
<replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable
class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ...
]
+
+<phrase>where <replaceable
class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

Same review comment as #3 before.

I think it is clearer (and more flexible) to change the
exception_object to include [TABLE].
[ TABLE ] [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

It also helps pave the way for any future EXCLUDE SEQUENCE feature.

~~~

7.
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If
<literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its
changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>

I felt that the second paragraph should be started with the sentence
"The partitioned table or its partitions are excluded...", so then
everything related to "publish_via_partition_root" is kept together.

~~~

8.
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>

The words "the changes of" are not needed, and you did not use that
wording in the ALTER PUBLICATION example.

======
doc/src/sgml/ref/psql-ref.sgml

9.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication
are shown as
+        well.
         </para>

/excluded tables and schemas/excluded tables, and schemas/

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

#129Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#128)
3 attachment(s)
Re: Skipping schema changes in publication

On Thu, 30 Oct 2025 at 11:34, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Vignesh

Here are some review comments for the patch v24-0002.

These comments are just for the SGML docs. The patch needs a rebase so
I was unable to review the code.

======
Commit message

1.
A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

~

/to maintain/to flag/

======
doc/src/sgml/logical-replication.sgml

2.
<para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
</para>

Those "FOR ALL TABLES" etc are missing SGML markup.

======
doc/src/sgml/ref/alter_publication.sgml

3.
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable
class="parameter">exception_object</replaceable> [, ... ] ]

and

+
+<phrase>where <replaceable
class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+

It is not clear from the syntax which of these is possible.

... ADD ALL TABLES EXCEPT TABLE t1,t2,t3
... ADD ALL TABLES EXCEPT TABLE t1, TABLE t2, TABLES t3

IMO it is best put the "[TABLE]" within the exception_object:
[ TABLE ] [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

Then both are possible, which is consistent with how "FOR TABLE" syntax works.

Furthermore, you might want later to say EXCLUDE SEQUENCE, so doing it
this way makes that possible.

~~~

4.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,

This wording seems a bit awkward. How are re-phrasing like:

SUGGESTION
Adding or excluding a table from a publication requires ownership of that table.

~~~

5.
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.

typo: /donot/does not/

======
doc/src/sgml/ref/create_publication.sgml

6.
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable
class="parameter">exception_object</replaceable> [, ... ] ]
| FOR <replaceable
class="parameter">publication_object</replaceable> [, ... ] ]
[ WITH ( <replaceable
class="parameter">publication_parameter</replaceable> [= <replaceable
class="parameter">value</replaceable>] [, ... ] ) ]

@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable
class="parameter">name</replaceable>

TABLE [ ONLY ] <replaceable
class="parameter">table_name</replaceable> [ * ] [ ( <replaceable
class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE (
<replaceable class="parameter">expression</replaceable> ) ] [, ... ]
TABLES IN SCHEMA { <replaceable
class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ...
]
+
+<phrase>where <replaceable
class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

Same review comment as #3 before.

I think it is clearer (and more flexible) to change the
exception_object to include [TABLE].
[ TABLE ] [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

It also helps pave the way for any future EXCLUDE SEQUENCE feature.

~~~

7.
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If
<literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its
changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>

I felt that the second paragraph should be started with the sentence
"The partitioned table or its partitions are excluded...", so then
everything related to "publish_via_partition_root" is kept together.

~~~

8.
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>

The words "the changes of" are not needed, and you did not use that
wording in the ALTER PUBLICATION example.

======
doc/src/sgml/ref/psql-ref.sgml

9.
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication
are shown as
+        well.
</para>

/excluded tables and schemas/excluded tables, and schemas/

Hi Peter, Vignesh

Thanks for reviewing the patches.
I have rebased the patches. I have modified the syntax for EXCEPT
TABLE (002) patch.
For example, now to exclude a table we need to specify like:
CREATE PUBLICATION pub1 FOR ALL TABLE EXCEPT TABLE (t1, t2);
We need to specify '()' around the table list.

This patchset is the only rebased version. I will address all the
comments in the next version of patch.

Thanks,
Shlok Kyal

Attachments:

v25-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v25-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 632a1f6bab7305b55821d7b3c6e42cd2bb11cd85 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 30 Oct 2025 10:52:56 +0530
Subject: [PATCH v25 1/3] Add RESET clause to Alter Publication which will 
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  34 ++++--
 src/backend/commands/publicationcmds.c    | 124 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |   9 ++
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 118 ++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  57 ++++++++++
 7 files changed, 331 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index c36e754f887..85c79c1a9cb 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,31 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> flag to <literal>false</literal>, and
+   removing all associated tables and schemas from the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
    or <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
@@ -232,6 +246,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1faf3a8c372..ed88c306677 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -48,6 +48,16 @@
 #include "utils/varlena.h"
 
 
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*
  * Information used to validate the columns in the row filter expression. See
  * contain_invalid_rfcolumn_walker for details.
@@ -90,12 +100,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1209,6 +1219,106 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballtables)
+	{
+		values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+		replaces[Anum_pg_publication_puballtables - 1] = true;
+		CacheInvalidateRelcacheAll();
+	}
+
+	/* Set ALL TABLES flag to false */
+	if (pubform->puballsequences)
+	{
+		values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
+		replaces[Anum_pg_publication_puballsequences - 1] = true;
+	}
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1553,6 +1663,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_ResetPublication)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 57fe0186547..2b88c74a319 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10909,6 +10909,8 @@ pub_obj_type_list:	PublicationAllObjSpec
  *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10955,6 +10957,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_ResetPublication;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 316a2dafbf1..d16181bc115 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2288,7 +2288,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..778ef039d86 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4326,6 +4326,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_ResetPublication,		/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..bb614ba5d0a 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2009,6 +2009,124 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                           Publication testpub_reset
+          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+SET ROLE regress_publication_user;
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..829466d8de0 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1268,6 +1268,63 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+
+-- Verify that associated schemas are reomved from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+
+-- Verify that 'PUBLISH' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
-- 
2.34.1

v25-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v25-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From fc872b2fb74037de4f88a6476e644f00e229a56c Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 6 Nov 2025 15:29:40 +0530
Subject: [PATCH v25 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 doc/src/sgml/system-views.sgml                |  10 +
 src/backend/catalog/pg_publication.c          |  86 +++++-
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     | 130 +++++++--
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/038_rep_changes_except_collist.pl       | 193 +++++++++++++
 23 files changed, 956 insertions(+), 245 deletions(-)
 create mode 100644 src/test/subscription/t/038_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 144f3fbdef2..c5e97d1575f 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6586,7 +6586,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 7359bd77569..42405c95687 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1374,10 +1374,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1391,8 +1391,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1404,6 +1407,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1425,11 +1436,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1474,18 +1488,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <literal>t1</literal> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <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. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1499,6 +1516,7 @@ Publications:
  postgres | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1519,23 +1537,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <literal>t1</literal> which now
-     only needs a subset of the columns that were on the publisher table
-     <literal>t1</literal>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <literal>t1</literal>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1547,11 +1583,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1560,6 +1606,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1656,6 +1709,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 4afe62b7cfd..62204b4a416 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
@@ -262,6 +262,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departmen
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index d28a9a10c86..21aec0ae1c7 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -27,7 +27,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
@@ -96,17 +96,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -368,10 +375,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -394,6 +403,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -516,6 +535,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all sequences for synchronization:
 <programlisting>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7971498fe75..243697d4d8c 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2698,6 +2698,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Expression for the table's publication qualifying condition
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>exceptcol</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if a column list with <literal>EXCEPT</literal> clause is specified
+       for the table in the publication.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index bec3a34e48f..02bba6b08cf 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -266,14 +266,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -299,6 +304,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -660,10 +675,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -686,6 +703,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -790,8 +810,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -831,10 +853,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1163,7 +1187,7 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 Datum
 pg_get_publication_tables(PG_FUNCTION_ARGS)
 {
-#define NUM_PUBLICATION_TABLES_ELEM	4
+#define NUM_PUBLICATION_TABLES_ELEM	5
 	FuncCallContext *funcctx;
 	List	   *table_infos = NIL;
 
@@ -1270,6 +1294,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						   INT2VECTOROID, -1, 0);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 4, "qual",
 						   PG_NODE_TREEOID, -1, 0);
+		TupleDescInitEntry(tupdesc, (AttrNumber) 5, "exceptcol",
+						   BOOLOID, -1, 0);
 
 		funcctx->tuple_desc = BlessTupleDesc(tupdesc);
 		funcctx->user_fctx = table_infos;
@@ -1291,6 +1317,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1315,11 +1342,36 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+				{
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+
+					if (except_columns)
+						values[4] = BoolGetDatum(true);
+					else
+						values[4] = BoolGetDatum(false);
+				}
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1331,8 +1383,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1363,6 +1419,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1371,6 +1434,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index dec8df4f8ee..9003f25fac4 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -388,7 +388,8 @@ CREATE VIEW pg_publication_tables AS
           WHERE a.attrelid = GPT.relid AND
                 a.attnum = ANY(GPT.attrs)
         ) AS attnames,
-        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
+        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter,
+        gpt.exceptcol AS exceptcol
     FROM pg_publication P,
          LATERAL pg_get_publication_tables(P.pubname) GPT,
          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index e70fdefbc90..11ec4e2818b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -237,7 +237,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -391,8 +390,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -414,6 +413,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -437,7 +437,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -527,8 +528,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1515,6 +1522,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1528,23 +1536,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1571,13 +1584,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 87e78f13d94..94f2e654551 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4480,6 +4480,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10796,14 +10801,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10819,7 +10825,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10827,7 +10833,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10837,8 +10843,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10846,25 +10853,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19856,6 +19865,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index e5a2856fd17..6ca32fd85bc 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -720,10 +720,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -737,6 +745,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -787,7 +798,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -801,7 +812,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -811,7 +832,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -858,6 +879,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -920,7 +944,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1052,11 +1077,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a9593c5d9da..7f534618cf4 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1091,12 +1101,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1145,19 +1164,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1169,7 +1210,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1179,9 +1220,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1505,6 +1548,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2078,6 +2128,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2127,6 +2178,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e34aaba7937..1fdb90f6482 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4933,24 +4933,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4976,10 +4959,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -5059,7 +5061,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 723b5575c53..ca2d356f72a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -690,6 +690,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 50b1d435359..6ceb108a35b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3053,16 +3138,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3072,35 +3168,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3121,34 +3249,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6532,49 +6634,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6764,6 +6823,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6772,11 +6837,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6788,8 +6850,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6799,14 +6861,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c5c8e6e8534..f72be93d942 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2294,6 +2294,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT (");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT"))
+		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -2314,10 +2318,13 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "WHERE", "("))
 		COMPLETE_WITH_ATTR(prev3_wd);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT", "("))
+		COMPLETE_WITH_ATTR(prev3_wd);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 !TailMatches("WHERE", "(*)"))
+			 !TailMatches("WHERE", "(*)") && !TailMatches("EXCEPT", "("))
 		COMPLETE_WITH(",", "WHERE (");
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 !ends_with(prev_wd, '('))
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
@@ -3635,7 +3642,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..6068abf3afa 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12298,9 +12298,9 @@
   proname => 'pg_get_publication_tables', prorows => '1000',
   provariadic => 'text', proretset => 't', provolatile => 's',
   prorettype => 'record', proargtypes => '_text',
-  proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
-  proargmodes => '{v,o,o,o,o}',
-  proargnames => '{pubname,pubid,relid,attrs,qual}',
+  proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree,bool}',
+  proargmodes => '{v,o,o,o,o,o}',
+  proargnames => '{pubname,pubid,relid,attrs,qual,exceptcol}',
   prosrc => 'pg_get_publication_tables' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 9a07215ae30..0ab8a008dc4 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -188,7 +188,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -198,6 +199,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 062469d7220..1747c22b2bc 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1904,52 +1904,52 @@ CREATE TABLE sch2.tbl1_part1 PARTITION OF sch1.tbl1 FOR VALUES FROM (1) to (10);
 -- Schema publication that does not include the schema that has the parent table
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
 -- Table publication that does not include the parent table
 CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 -- Table publication that includes both the parent table and the child table
 ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename | attnames | rowfilter 
----------+------------+-----------+----------+-----------
- pub     | sch1       | tbl1      | {a}      | 
+ pubname | schemaname | tablename | attnames | rowfilter | exceptcol 
+---------+------------+-----------+----------+-----------+-----------
+ pub     | sch1       | tbl1      | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
 -- Schema publication that does not include the schema that has the parent table
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
 -- Table publication that does not include the parent table
 CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 -- Table publication that includes both the parent table and the child table
 ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | exceptcol 
+---------+------------+------------+----------+-----------+-----------
+ pub     | sch2       | tbl1_part1 | {a}      |           | f
 (1 row)
 
 DROP PUBLICATION pub;
@@ -1962,9 +1962,9 @@ CREATE TABLE sch1.tbl1_part3 (a int) PARTITION BY RANGE(a);
 ALTER TABLE sch1.tbl1 ATTACH PARTITION sch1.tbl1_part3 FOR VALUES FROM (20) to (30);
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename | attnames | rowfilter 
----------+------------+-----------+----------+-----------
- pub     | sch1       | tbl1      | {a}      | 
+ pubname | schemaname | tablename | attnames | rowfilter | exceptcol 
+---------+------------+-----------+----------+-----------+-----------
+ pub     | sch1       | tbl1      | {a}      |           | f
 (1 row)
 
 RESET client_min_messages;
@@ -2211,6 +2211,94 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter | exceptcol 
+----------------+------------+------------------+-----------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} |           | f
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     |           | t
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2bf968ae3d3..7a030f30155 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1476,9 +1476,10 @@ pg_publication_tables| SELECT p.pubname,
     ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg
            FROM pg_attribute a
           WHERE ((a.attrelid = gpt.relid) AND (a.attnum = ANY ((gpt.attrs)::smallint[])))) AS attnames,
-    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter
+    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter,
+    gpt.exceptcol
    FROM pg_publication p,
-    LATERAL pg_get_publication_tables(VARIADIC ARRAY[(p.pubname)::text]) gpt(pubid, relid, attrs, qual),
+    LATERAL pg_get_publication_tables(VARIADIC ARRAY[(p.pubname)::text]) gpt(pubid, relid, attrs, qual, exceptcol),
     (pg_class c
      JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
   WHERE (c.oid = gpt.relid);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index f5c62338e78..9266071d618 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1364,6 +1364,61 @@ SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index b8e5c54c314..e8e69f7443d 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -47,6 +47,7 @@ tests += {
       't/035_conflicts.pl',
       't/036_sequences.pl',
       't/037_rep_changes_except_table.pl',
+      't/038_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/038_rep_changes_except_collist.pl b/src/test/subscription/t/038_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..3dfd266bc3d
--- /dev/null
+++ b/src/test/subscription/t/038_rep_changes_except_collist.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'Verify initial sync of tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'Verify initial sync of sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'Verify incremental inserts on tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||),
+	'Verify incremental inserts on sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v25-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v25-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From f495af76ef8ea7b3fc4a779423b94da1090707cb Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 6 Nov 2025 14:19:22 +0530
Subject: [PATCH v25 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   8 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  49 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  99 ++++---
 src/backend/commands/publicationcmds.c        | 246 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 +++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  22 +-
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   4 +
 src/test/regress/expected/publication.out     | 185 +++++++++----
 src/test/regress/sql/publication.sql          |  45 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 186 +++++++++++++
 25 files changed, 947 insertions(+), 185 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6c8a0f173c9..144f3fbdef2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index daab2cae989..7359bd77569 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2301,10 +2301,10 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 85c79c1a9cb..4afe62b7cfd 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">exception_object</replaceable> [, ... ] ) ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -88,8 +94,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD ALL TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -130,7 +137,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.
      </para>
 
      <para>
@@ -239,6 +247,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departments);
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 66a70e5c5b5..d28a9a10c86 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,8 +32,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
+
+<phrase>where <replaceable class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -160,7 +164,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -180,6 +186,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -463,6 +499,15 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1ab427d18af..9998ab32565 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication are shown as
+        well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ac2f4ee3561..bec3a34e48f 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +479,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +506,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,9 +775,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -774,13 +801,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
  * should use GetAllPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +833,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -866,13 +897,19 @@ GetAllTablesPublications(void)
  * publication.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +928,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +950,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1207,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1178,7 +1218,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1408,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = (void *) sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index ed88c306677..e70fdefbc90 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -180,6 +180,39 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -204,6 +237,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -278,7 +316,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -305,7 +343,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -365,7 +404,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -389,7 +428,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -525,7 +565,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -933,56 +973,54 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
-	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1051,7 +1089,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1171,7 +1209,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1267,6 +1305,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
 	replaces[Anum_pg_publication_pubgencols - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	/* Set ALL TABLES flag to false */
 	if (pubform->puballtables)
 	{
@@ -1301,7 +1360,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1319,6 +1381,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1357,7 +1492,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1458,6 +1594,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1508,7 +1645,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1661,6 +1799,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_ResetPublication)
@@ -1873,6 +2025,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1945,6 +2098,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3aac459e483..7fc8f08b0eb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2b88c74a319..87e78f13d94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_obj_type_list
+				except_pub_obj_list except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> ExceptPublicationObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10802,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10878,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = (List *) $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10898,6 +10904,28 @@ pub_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+except_clause:
+			EXCEPT opt_table '(' except_pub_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list: ExceptPublicationObjSpec
+					{ $$ = list_make1($1); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
@@ -10911,6 +10939,8 @@ pub_obj_type_list:	PublicationAllObjSpec
  *
  * ALTER PUBLICATION name RESET
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] (table_name [, ...])
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
@@ -10937,6 +10967,15 @@ AlterPublicationStmt:
 					n->action = AP_AddObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES except_clause
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = (List *) $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name SET pub_obj_list
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 942e1abdb58..a9593c5d9da 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2195,22 +2196,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2214,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2230,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2310,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc9084..96dd0ccf41a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a00918bacb4..e34aaba7937 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4662,7 +4664,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4831,6 +4860,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4842,8 +4872,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4854,6 +4892,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4869,6 +4908,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4880,6 +4920,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4893,7 +4934,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4934,6 +4979,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11812,6 +11860,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20182,6 +20233,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..f3c30f3be37 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -443,6 +445,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1715,6 +1728,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 445a541abf6..156319b8038 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
@@ -5157,7 +5177,7 @@ foreach my $run (sort keys %pgdump_runs)
 		#
 		# Either "all_runs" should be set or there should be a "like" list,
 		# even if it is empty.  (This makes the test more self-documenting.)
-		if (!defined($tests{$test}->{all_runs})
+		if (   !defined($tests{$test}->{all_runs})
 			&& !defined($tests{$test}->{like}))
 		{
 			die "missing \"like\" in test \"$test\"";
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index d16181bc115..c5c8e6e8534 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2291,11 +2291,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3622,6 +3627,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..9a07215ae30 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -168,9 +169,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -181,7 +183,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 778ef039d86..305c246ea09 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List *except_objects;	/* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
@@ -4342,6 +4345,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index bb614ba5d0a..062469d7220 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
@@ -2012,110 +2053,150 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | t          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
     "pub_sch1.tbl1"
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables from schemas:
     "public"
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | f       | f       | f       | f         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | f       | f       | f       | f         | none              | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | t
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | t
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
 -- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | stored            | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | stored            | f
 (1 row)
 
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
-                                           Publication testpub_reset
-          Owner           | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
---------------------------+------------+---------+---------+---------+-----------+-------------------+----------
- regress_publication_user | f          | t       | t       | t       | t         | none              | f
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
 -- Verify that only superuser can reset a publication
@@ -2123,9 +2204,13 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  must be superuser to ADD ALL TABLES to the publication
 SET ROLE regress_publication_user;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 829466d8de0..f5c62338e78 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
@@ -1271,17 +1284,30 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+
+ALTER PUBLICATION testpub_reset RESET;
+
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
 
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1289,6 +1315,9 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that associated schemas are reomved from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1296,6 +1325,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
 
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1303,6 +1336,10 @@ ALTER PUBLICATION testpub_reset RESET;
 
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
 
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
@@ -1319,10 +1356,14 @@ ALTER PUBLICATION testpub_reset RESET;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 SET ROLE regress_publication_user;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..096e0606365
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE (sch1.tab1, public.tab1);
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.t1);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.part1);
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#130Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#129)
Re: Skipping schema changes in publication

Hi Shlok.

Here are some review comments for the patch v25-0001 (RESET).

I belatedly saw that you said this is a rebase *only*, so does not yet
address any of the earlier review comments [1]my review of v24-0001 /messages/by-id/CAHut+PvoOVo=_O-sG8wNaLRBPSD+6S=4PXOH2r=yKTxbpAbHkg@mail.gmail.com. Anyway, below are a
few more comments that I did not report previously.

======
Commit message

1.
This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state which includes resetting the publication
parameters, setting ALL TABLES flag to false and dropping the relations and
schemas that are associated with the publication.

~

1a.
/which includes.../. This includes...

~

1b.
Needs to also mention about ALL SEQUENCES

======
src/backend/commands/publicationcmds.c

AlterPublicationReset:

2.
+ /* Set ALL TABLES flag to false */
+ if (pubform->puballsequences)
+ {
+ values[Anum_pg_publication_puballsequences - 1] =
BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
+ replaces[Anum_pg_publication_puballsequences - 1] = true;
+ }

The comment should say ALL SEQUENCES.

======
[1]: my review of v24-0001 /messages/by-id/CAHut+PvoOVo=_O-sG8wNaLRBPSD+6S=4PXOH2r=yKTxbpAbHkg@mail.gmail.com
/messages/by-id/CAHut+PvoOVo=_O-sG8wNaLRBPSD+6S=4PXOH2r=yKTxbpAbHkg@mail.gmail.com

Kind Regards,
Peter Smith.
Fujitsu Australia

#131Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#129)
Re: Skipping schema changes in publication

Hi Shlok.

This is a general comment about the content of these patches.

IIUC, the v25* patches currently are currently arranged like this:

0001
- New command ALTER PUBLICATION pubname RESET;
0002
- Add new command: ALTER PUBLICATION pub_name ADD ALL TABLES;
- Enhance existing CREATE and the new ALTER syntax for EXCEPT tables
0003
- Enhance existing CREATE and ALTER syntax for EXCEPT col_list

~~~

IMO it is a bug that the ALTER PUBLICATION pub_name ADD/SET ALL TABLES
command does not already exist as a supported command. And, that is
independent of anything else you are implementing here like RESET or
EXCEPT.

Therefore, I think that one should be 1st in your patchset; The EXCEPT
stuff then just becomes enhancements to existing syntax, which would
give a cleaner separation of logic.

So, I am suggesting there should be 4 patches instead of 3. e.g.

SUGGESTION
0001 - New command: ALTER PUBLICATION pub_name ADD/SET ALL TABLES;
0002 - New command: ALTER PUBLICATION pubname RESET;
0003 - Enhance existing CREATE/ALTER syntax for EXCEPT tables
0004 - Enhance existing CREATE/ALTER syntax for EXCEPT col_list

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

#132Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#129)
Re: Skipping schema changes in publication

Hi Shlok.

Some questions for the patch v25-0002 (EXCEPT tables)

======
doc/src/sgml/ref/alter_publication.sgml

1.
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable
class="parameter">exception_object</replaceable> [, ... ] ) ]

You can do both ADD/SET the <publication_object>, so really there
should be an ADD/SET ALL TABLES command as well, right?

~~~

2.
What was your reason for changing the syntax?
AFAICT those added "( )" are not strictly necessary, so I just
wondered your reason.

For example, we do not have any "( )" for <publication_object> [,...].
It is: ALTER PUBLICATION name ADD publication_object [, ...]
Not: ALTER PUBLICATION name ADD (publication_object [, ...])

So in the same way we could have EXCEPT syntax like that:
ALTER PUBLICATION name ADD ALL TABLES [EXCEPT <table_exception_object> [, ...]]
Where table_exception_object is: [ TABLE ] [ ONLY ] table_name [ * ]

Currently, if the user just wants to exclude a single table they must do:
ALTER PUBLICATION name ADD ALL TABLES EXCEPT (t1);
instead of just ALTER PUBLICATION name ADD ALL TABLES EXCEPT t1;

~~~

3.
BTW, I think you may need to consider a <table_exception_object>
instead of a generic name like <exception_object>, because in the
future if we EXCEPT SEQUENCES the <exception_object> name may be not
appropriate because things like [ONLY] and [*] are not applicable for
sequences.

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

#133Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#132)
3 attachment(s)
Re: Skipping schema changes in publication

On Fri, 7 Nov 2025 at 11:36, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Some questions for the patch v25-0002 (EXCEPT tables)

======
doc/src/sgml/ref/alter_publication.sgml

1.
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable
class="parameter">exception_object</replaceable> [, ... ] ) ]

You can do both ADD/SET the <publication_object>, so really there
should be an ADD/SET ALL TABLES command as well, right?

These patches only added the ADD ALL TABLES command. I think once the
ADD ALL TABLES patch is committed, we can add the syntax SET ALL
TABLES.

~~~

2.
What was your reason for changing the syntax?
AFAICT those added "( )" are not strictly necessary, so I just
wondered your reason.

For example, we do not have any "( )" for <publication_object> [,...].
It is: ALTER PUBLICATION name ADD publication_object [, ...]
Not: ALTER PUBLICATION name ADD (publication_object [, ...])

So in the same way we could have EXCEPT syntax like that:
ALTER PUBLICATION name ADD ALL TABLES [EXCEPT <table_exception_object> [, ...]]
Where table_exception_object is: [ TABLE ] [ ONLY ] table_name [ * ]

Currently, if the user just wants to exclude a single table they must do:
ALTER PUBLICATION name ADD ALL TABLES EXCEPT (t1);
instead of just ALTER PUBLICATION name ADD ALL TABLES EXCEPT t1;

With recent commit now we support
CREATE PUBLICATION .. FOR ALL TABLES, ALL SEQUENCES.

Now when I am trying to support "FOR ALL TABLE EXCEPT t1, t2" , I am
getting a conflict when compiling this grammar.
For example
CREATE PUBLICATION .. FOR ALL TABLES EXCEPT t1, ...
After this comma, bison is giving conflict because it is not able to
figure whether to pick
ExceptPublicationObjSpec or a PublicationAllObjSpec.
So to handle this I introduced brackets around the table list.
And to make ALTER PUBLICATION similar to CREATE PUBLICATION, I have
added the same syntax for it.

So current syntax for CREATE/ALTER PUBLICATION is like:
CREATE PUBLICATION ... ALL TABLES EXCEPT TABLE(t1, t2, t3);
ALTER PUBLICATION ... ADD ALL TABLES EXCEPT TABLE(t1, t2, t3);

~~~

3.
BTW, I think you may need to consider a <table_exception_object>
instead of a generic name like <exception_object>, because in the
future if we EXCEPT SEQUENCES the <exception_object> name may be not
appropriate because things like [ONLY] and [*] are not applicable for
sequences.

Fixed

I have attached the latest patch here.
I have also addressed the comments for [1]/messages/by-id/CALDaNm0xDv96F+5LzcJYV6RC3Jg+RtdUqpQ-zoauwq3woTFzmQ@mail.gmail.com, [2]/messages/by-id/CAHut+PsRD8ybC7MDBNBXXs=J2DuGiOc8kSePRyZc0s63U5f7tw@mail.gmail.com.

[1]: /messages/by-id/CALDaNm0xDv96F+5LzcJYV6RC3Jg+RtdUqpQ-zoauwq3woTFzmQ@mail.gmail.com
[2]: /messages/by-id/CAHut+PsRD8ybC7MDBNBXXs=J2DuGiOc8kSePRyZc0s63U5f7tw@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v26-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v26-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 97785ed9fa05e04bbe87587ccb979af79e11b721 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 30 Oct 2025 10:52:56 +0530
Subject: [PATCH v26 1/3] Add RESET clause to Alter Publication which will 
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state. This includes resetting the publication
parameters, setting ALL TABLES and ALL SEQUENCES flags to false and dropping
the relations and schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 +++++--
 src/backend/commands/publicationcmds.c    | 108 ++++++++++++++++++--
 src/backend/parser/gram.y                 |  13 ++-
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/catalog/pg_publication.h      |  10 ++
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 119 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  53 ++++++++++
 8 files changed, 325 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index c36e754f887..a9db3564474 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -69,18 +70,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> and <literal>ALL SEQUENCES</literal> flags to
+   <literal>false</literal>, and removing all associated tables and schemas from
+   the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
    or <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
@@ -232,6 +247,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1faf3a8c372..50239513e3f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -90,12 +90,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1209,6 +1209,100 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
+	replaces[Anum_pg_publication_puballsequences - 1] = true;
+
+	if (pubform->puballtables)
+		CacheInvalidateRelcacheAll();
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1553,6 +1647,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_Reset)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 57fe0186547..c9e83f05af9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10905,15 +10905,17 @@ pub_obj_type_list:	PublicationAllObjSpec
  *
  * ALTER PUBLICATION name ADD pub_obj [, ...]
  *
- * ALTER PUBLICATION name DROP pub_obj [, ...]
- *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name DROP pub_obj [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  *****************************************************************************/
 
 AlterPublicationStmt:
@@ -10955,6 +10957,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_Reset;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 316a2dafbf1..d16181bc115 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2288,7 +2288,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..77b0a2f9eb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -152,6 +152,16 @@ extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
 
+/* default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
  * which allows callers to specify which partitions of partitioned tables
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..8cf75724a7b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4326,6 +4326,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_Reset,					/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..e3be29e378d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2009,6 +2009,125 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that associated tables are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that associated schemas are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that 'PUBLISH' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..84deaaf5a1f 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1268,6 +1268,59 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that associated tables are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that associated schemas are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that 'PUBLISH' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
-- 
2.34.1

v26-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v26-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 07a9626ae059a406527ab167dea3cbd7486781fe Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Nov 2025 13:06:27 +0530
Subject: [PATCH v26 3/3] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/038_rep_changes_except_collist.pl       | 193 +++++++++++++
 19 files changed, 907 insertions(+), 217 deletions(-)
 create mode 100644 src/test/subscription/t/038_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 144f3fbdef2..c5e97d1575f 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6586,7 +6586,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 7147c190ba4..f4e0958ed7a 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1374,10 +1374,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1391,8 +1391,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1404,6 +1407,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1425,11 +1436,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1474,18 +1488,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <structname>t1</structname> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <para>
     Create a publication <literal>p1</literal>. A column list is defined for
-    table <structname>t1</structname> to reduce the number of columns that will be
-    replicated. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1499,6 +1516,7 @@ Publications:
  postgres | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1519,23 +1537,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <structname>t1</structname> which now
-     only needs a subset of the columns that were on the publisher table
-     <structname>t1</structname>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <structname>t1</structname>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1547,11 +1583,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1560,6 +1606,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1656,6 +1709,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index e0e940ff8c3..1f48a94e1f2 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
@@ -263,6 +263,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departmen
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 8b616651272..9f581e96440 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -27,7 +27,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
@@ -96,17 +96,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -367,10 +374,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -393,6 +402,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -514,6 +533,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all sequences for synchronization:
 <programlisting>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index bec3a34e48f..02a1203dcad 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -266,14 +266,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -299,6 +304,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -660,10 +675,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -686,6 +703,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -790,8 +810,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -831,10 +853,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1291,6 +1315,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1315,11 +1340,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1331,8 +1374,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1363,6 +1410,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1371,6 +1425,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6fb69e3f3ba..04e75c5ef1c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -227,7 +227,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -381,8 +380,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -404,6 +403,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -427,7 +427,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -517,8 +518,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1500,6 +1507,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1513,23 +1521,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1556,13 +1569,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e43790ae3fd..587640ed3f6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4480,6 +4480,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10796,14 +10801,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10819,7 +10825,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10827,7 +10833,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10837,8 +10843,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10846,25 +10853,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19856,6 +19865,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index dcc6124cc73..29a453d5f63 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -720,10 +720,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -737,6 +745,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -787,7 +798,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -801,7 +812,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -811,7 +832,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -858,6 +879,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -920,7 +944,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1052,11 +1077,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a9593c5d9da..7f534618cf4 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1091,12 +1101,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1145,19 +1164,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1169,7 +1210,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1179,9 +1220,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1505,6 +1548,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2078,6 +2128,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2127,6 +2178,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e34aaba7937..1fdb90f6482 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4933,24 +4933,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4976,10 +4959,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -5059,7 +5061,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 723b5575c53..ca2d356f72a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -690,6 +690,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 50b1d435359..6ceb108a35b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3053,16 +3138,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3072,35 +3168,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3121,34 +3249,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6532,49 +6634,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6764,6 +6823,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6772,11 +6837,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6788,8 +6850,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6799,14 +6861,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c5c8e6e8534..f72be93d942 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2294,6 +2294,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT (");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT"))
+		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -2314,10 +2318,13 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "WHERE", "("))
 		COMPLETE_WITH_ATTR(prev3_wd);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT", "("))
+		COMPLETE_WITH_ATTR(prev3_wd);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 !TailMatches("WHERE", "(*)"))
+			 !TailMatches("WHERE", "(*)") && !TailMatches("EXCEPT", "("))
 		COMPLETE_WITH(",", "WHERE (");
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 !ends_with(prev_wd, '('))
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
@@ -3635,7 +3642,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 0d39cb67779..594a2e14676 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -198,7 +198,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -208,6 +209,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 55cc7d5ee71..d38b0939c16 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2214,6 +2214,94 @@ ALTER PUBLICATION testpub_reset RESET;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index d3c03f54278..c917bb000aa 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1359,6 +1359,61 @@ ALTER PUBLICATION testpub_reset RESET;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index b8e5c54c314..e8e69f7443d 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -47,6 +47,7 @@ tests += {
       't/035_conflicts.pl',
       't/036_sequences.pl',
       't/037_rep_changes_except_table.pl',
+      't/038_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/038_rep_changes_except_collist.pl b/src/test/subscription/t/038_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..3dfd266bc3d
--- /dev/null
+++ b/src/test/subscription/t/038_rep_changes_except_collist.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'Verify initial sync of tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'Verify initial sync of sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'Verify incremental inserts on tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||),
+	'Verify incremental inserts on sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v26-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v26-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From ddd01800c139d66d3f5bc39db174657fce94fe0f Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Nov 2025 13:52:17 +0530
Subject: [PATCH v26 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |   9 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  47 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  99 +++++--
 src/backend/commands/publicationcmds.c        | 247 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 +++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  22 +-
 src/bin/psql/describe.c                       |  58 +++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   4 +
 src/test/regress/expected/publication.out     |  99 ++++++-
 src/test/regress/sql/publication.sql          |  52 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 186 +++++++++++++
 25 files changed, 908 insertions(+), 145 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6c8a0f173c9..144f3fbdef2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index d64ed9dc36b..7147c190ba4 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2301,10 +2301,11 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index a9db3564474..e0e940ff8c3 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
 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>] [, ... ] )
@@ -33,6 +34,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -89,8 +95,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding or excluding a table from a publication requires ownership of that
+   table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -131,7 +138,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> does not have any effect.
      </para>
 
      <para>
@@ -240,6 +248,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departments);
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 66a70e5c5b5..8b616651272 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,8 +32,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -160,7 +164,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -180,6 +186,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>
+     <para>
+      The partitioned table or its partitions are excluded from the publication
+      based on the parameter <literal>publish_via_partition_root</literal>.
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -463,6 +498,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ac2f4ee3561..bec3a34e48f 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +479,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +506,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,9 +775,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -774,13 +801,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
  * should use GetAllPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +833,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -866,13 +897,19 @@ GetAllTablesPublications(void)
  * publication.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +928,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +950,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1207,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1178,7 +1218,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1408,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = (void *) sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 50239513e3f..6fb69e3f3ba 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,39 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +227,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +306,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +333,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -355,7 +394,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +418,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -515,7 +555,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -923,56 +963,54 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
-	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1041,7 +1079,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1161,7 +1199,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1263,6 +1301,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
 	replaces[Anum_pg_publication_puballsequences - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	if (pubform->puballtables)
 		CacheInvalidateRelcacheAll();
 
@@ -1285,7 +1344,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1303,6 +1365,80 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES ||
+		pubform->puballsequences != PUB_DEFAULT_ALL_SEQUENCES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1341,7 +1477,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1442,6 +1579,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1492,7 +1630,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1645,6 +1784,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_Reset)
@@ -1857,6 +2010,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1929,6 +2083,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3aac459e483..7fc8f08b0eb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9e83f05af9..e43790ae3fd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_obj_type_list
+				except_pub_obj_list opt_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> ExceptPublicationObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10802,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10878,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = (List *) $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10898,6 +10904,28 @@ pub_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_except_clause:
+			EXCEPT opt_table '(' except_pub_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list: ExceptPublicationObjSpec
+					{ $$ = list_make1($1); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
@@ -10914,6 +10942,8 @@ pub_obj_type_list:	PublicationAllObjSpec
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] (table_name [, ...])
+ *
  * ALTER PUBLICATION name RESET
  *
  *****************************************************************************/
@@ -10957,6 +10987,15 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES opt_except_clause
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = (List *) $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RESET
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 942e1abdb58..a9593c5d9da 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2195,22 +2196,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2214,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2230,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2310,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc9084..96dd0ccf41a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a00918bacb4..e34aaba7937 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4662,7 +4664,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4831,6 +4860,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4842,8 +4872,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4854,6 +4892,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4869,6 +4908,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4880,6 +4920,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4893,7 +4934,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4934,6 +4979,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11812,6 +11860,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20182,6 +20233,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..f3c30f3be37 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -443,6 +445,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1715,6 +1728,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 445a541abf6..156319b8038 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
@@ -5157,7 +5177,7 @@ foreach my $run (sort keys %pgdump_runs)
 		#
 		# Either "all_runs" should be set or there should be a "like" list,
 		# even if it is empty.  (This makes the test more self-documenting.)
-		if (!defined($tests{$test}->{all_runs})
+		if (   !defined($tests{$test}->{all_runs})
 			&& !defined($tests{$test}->{like}))
 		{
 			die "missing \"like\" in test \"$test\"";
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index d16181bc115..c5c8e6e8534 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2291,11 +2291,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3622,6 +3627,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 77b0a2f9eb8..0d39cb67779 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /* default values for flags and publication parameters */
 #define PUB_DEFAULT_ACTION_INSERT true
@@ -178,9 +179,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -191,7 +193,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8cf75724a7b..a14ecedb27f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
@@ -4342,6 +4345,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e3be29e378d..55cc7d5ee71 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
@@ -2012,6 +2053,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
@@ -2020,8 +2062,18 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
 SET ROLE regress_publication_user;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                                    Publication testpub_reset
@@ -2038,8 +2090,25 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that associated tables are removed from the publication after RESET
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2056,8 +2125,13 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that associated schemas are removed from the publication after RESET
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that associated schemas are removed from the publication after RESET
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2074,8 +2148,14 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that 'PUBLISH' parameter is reset
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2091,8 +2171,14 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2127,6 +2213,7 @@ ALTER PUBLICATION testpub_reset RESET;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 84deaaf5a1f..d3c03f54278 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
@@ -1271,6 +1284,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
@@ -1279,34 +1293,59 @@ RESET client_min_messages;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
 SET ROLE regress_publication_user;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that associated tables are removed from the publication after RESET
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that associated schemas are removed from the publication after RESET
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that associated schemas are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that 'PUBLISH' parameter is reset
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1319,6 +1358,7 @@ ALTER PUBLICATION testpub_reset RESET;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..096e0606365
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE (sch1.tab1, public.tab1);
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.t1);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.part1);
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#134Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#131)
Re: Skipping schema changes in publication

On Fri, 7 Nov 2025 at 09:34, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

This is a general comment about the content of these patches.

IIUC, the v25* patches currently are currently arranged like this:

0001
- New command ALTER PUBLICATION pubname RESET;
0002
- Add new command: ALTER PUBLICATION pub_name ADD ALL TABLES;
- Enhance existing CREATE and the new ALTER syntax for EXCEPT tables
0003
- Enhance existing CREATE and ALTER syntax for EXCEPT col_list

~~~

IMO it is a bug that the ALTER PUBLICATION pub_name ADD/SET ALL TABLES
command does not already exist as a supported command. And, that is
independent of anything else you are implementing here like RESET or
EXCEPT.

Therefore, I think that one should be 1st in your patchset; The EXCEPT
stuff then just becomes enhancements to existing syntax, which would
give a cleaner separation of logic.

So, I am suggesting there should be 4 patches instead of 3. e.g.

SUGGESTION
0001 - New command: ALTER PUBLICATION pub_name ADD/SET ALL TABLES;
0002 - New command: ALTER PUBLICATION pubname RESET;
0003 - Enhance existing CREATE/ALTER syntax for EXCEPT tables
0004 - Enhance existing CREATE/ALTER syntax for EXCEPT col_list

I read the previous conversation in the thread. And got an
understanding that RESET was introduced so that we can have a way to
remove 'EXCEPT TABLE' from a publication and after RESET we can use
'ADD ALL TABLES [EXCEPT]' to alter the list of EXCEPT TABLE. So I
prefer to keep 'ALTER PUBLICATION .. RESET' as the first patch.
I think since 'ADD ALL TABLES' serves our current purpose. We can add
the syntax 'SET ALL TABLES' once 'ADD ALL TABLES' is in committed or
in committable shape.

Thanks,
Shlok Kyal

#135Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#127)
Re: Skipping schema changes in publication

On Fri, 24 Oct 2025 at 06:41, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Vignesh,

I had a look at patch v24-0001.

FYI -- a rebase is needed

[postgres@CentOS7-x64 oss_postgres_misc]$ git apply
../patches_misc/v24-0001-Add-RESET-clause-to-Alter-Publication-which-will.patch
error: patch failed: doc/src/sgml/ref/alter_publication.sgml:69
error: doc/src/sgml/ref/alter_publication.sgml: patch does not apply

Here are some other review comments:

======

1.
There seems to be some basic omission of the ALTER PUBLICATION in that
it does not support "ALL TABLES" as a publication_object.

Therefore, if you have:
CREATE PUBLICATION mypub FOR ALL TABLES;

and then you do:
ALTER PUBLICATION mypub RESET;

There seems to be no way to restore mpub to be an ALL TABLES publication again!

~~~

I think if you are going to implement a RESET, then you also need a
way to get back to what you had before you did the reset. So you'll
also need to implement the ALTER PUBLICATION mypub SET ALL TABLES;

IMO, supporting "SET ALL TABLES" should be your new 0001 patch
because AFAIK, this situation already exists if the user had created
an "empty" publication:
CREATE PUBLICATION myemptypub;

With current patches we can add ALL TABLES to a publication using "ADD
ALL TABLES"
I think once the ADD ALL TABLES patch is committed, we can add SET ALL TABLES.

======
doc/src/sgml/ref/alter_publication.sgml

2.
Probably need to mention ALL SEQUENCES now too?

======
src/backend/commands/publicationcmds.c

3.
+/* CREATE PUBLICATION default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+

Is it better to put all these in the catalog/pg_publication.h where
the catalog was defined?

~~~

AlterPublicationReset:

4.
+ /* Set ALL TABLES flag to false */
+ if (pubform->puballtables)
+ {
+ values[Anum_pg_publication_puballtables - 1] =
BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+ replaces[Anum_pg_publication_puballtables - 1] = true;
+ CacheInvalidateRelcacheAll();
+ }

Why not just do this anyway without the condition?

======
src/backend/parser/gram.y

6.
It would be nicer if all these grammar productions were coded in the
same order as the comment above them.

======
src/include/nodes/parsenodes.h

7.
AP_AddObjects, /* add objects to publication */
AP_DropObjects, /* remove objects from publication */
AP_SetObjects, /* set list of objects */
+ AP_ResetPublication, /* reset the publication */
} AlterPublicationAction;

It is already called "AlterPublicationAction", so maybe the enum value
only needs to be AP_Reset instead of AP_ResetPublication.

======
src/test/regress/expected/publication.out

8.
Expected output all needs rebasing now that there is a new "All
sequences" column.

======
src/test/regress/sql/publication.sql

9.
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+
+-- Verify that associated tables are removed from the publication after RESET
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+

I felt the ADD TABLE should be after the comment.

And ditto for all the other test cases -- should be that same pattern too.

# comment about test
ALTER .. do something
\dRp+ pub
ALTER .. RESET
\dRp+ pub

~~~

10.
+-- Verify that associated schemas are reomved from the publication after RESET

typo: /reomved/removed/

~~~

11.
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+SET ROLE regress_publication_user;
+

Perhaps this should be the first test?

======

I have addressed the remaining comments in the latest v26 patch [1]/messages/by-id/CANhcyEWGiWwGt1-v6d9bCAae9Np7cNPt+iRV9PXBZ0z=75XEVw@mail.gmail.com.
[1]: /messages/by-id/CANhcyEWGiWwGt1-v6d9bCAae9Np7cNPt+iRV9PXBZ0z=75XEVw@mail.gmail.com

Thanks,
Shlok Kyal

#136Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#128)
Re: Skipping schema changes in publication

On Thu, 30 Oct 2025 at 11:34, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Vignesh

Here are some review comments for the patch v24-0002.

These comments are just for the SGML docs. The patch needs a rebase so
I was unable to review the code.

======
Commit message

1.
A new column "prexcept" is added to table "pg_publication_rel", to maintain
the relations that the user wants to exclude from the publications.

~

/to maintain/to flag/

======
doc/src/sgml/logical-replication.sgml

2.
<para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables or
all tables in
-   schema automatically, the user must be a superuser.
+   To create a publication using FOR ALL TABLES or FOR ALL TABLES IN SCHEMA,
+   the user must be a superuser. To add ALL TABLES or ALL TABLES IN SCHEMA to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
</para>

Those "FOR ALL TABLES" etc are missing SGML markup.

======
doc/src/sgml/ref/alter_publication.sgml

3.
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] <replaceable
class="parameter">exception_object</replaceable> [, ... ] ]

and

+
+<phrase>where <replaceable
class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+

It is not clear from the syntax which of these is possible.

... ADD ALL TABLES EXCEPT TABLE t1,t2,t3
... ADD ALL TABLES EXCEPT TABLE t1, TABLE t2, TABLES t3

IMO it is best put the "[TABLE]" within the exception_object:
[ TABLE ] [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

Then both are possible, which is consistent with how "FOR TABLE" syntax works.

Furthermore, you might want later to say EXCLUDE SEQUENCE, so doing it
this way makes that possible.

Recently a commit was pushed which allowed use of ALL SEQUENCES
syntax. Due to it, I have updated the syntax to
CREATE PUBLICATION ... ALL TABLES EXCEPT TABLE(t1, t2, t3);

For ALTER PUBLICATION to have a similar syntax. I have updated it to
have syntax like:
ALTER PUBLICATION ... ADD ALL TABLES EXCEPT TABLE(t1, t2, t3);

I think in the future we can extend the syntax for sequences like:
ALL SEQUENCES EXCEPT(s1, s2, s3).

See [1]/messages/by-id/CANhcyEWGiWwGt1-v6d9bCAae9Np7cNPt+iRV9PXBZ0z=75XEVw@mail.gmail.com for more info.

~~~

4.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding a table to or excluding a table from a publication additionally
+   requires owning that table. The <literal>ADD ALL TABLES</literal>,

This wording seems a bit awkward. How are re-phrasing like:

SUGGESTION
Adding or excluding a table from a publication requires ownership of that table.

~~~

5.
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> donot have any effect.

typo: /donot/does not/

======
doc/src/sgml/ref/create_publication.sgml

6.
-    [ FOR ALL TABLES
+    [ FOR ALL TABLES [ EXCEPT [ TABLE ] <replaceable
class="parameter">exception_object</replaceable> [, ... ] ]
| FOR <replaceable
class="parameter">publication_object</replaceable> [, ... ] ]
[ WITH ( <replaceable
class="parameter">publication_parameter</replaceable> [= <replaceable
class="parameter">value</replaceable>] [, ... ] ) ]

@@ -30,6 +30,10 @@ CREATE PUBLICATION <replaceable
class="parameter">name</replaceable>

TABLE [ ONLY ] <replaceable
class="parameter">table_name</replaceable> [ * ] [ ( <replaceable
class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE (
<replaceable class="parameter">expression</replaceable> ) ] [, ... ]
TABLES IN SCHEMA { <replaceable
class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ...
]
+
+<phrase>where <replaceable
class="parameter">exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

Same review comment as #3 before.

I think it is clearer (and more flexible) to change the
exception_object to include [TABLE].
[ TABLE ] [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]

It also helps pave the way for any future EXCLUDE SEQUENCE feature.

See #3 before
.

~~~

7.
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If
<literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.  The partitioned
+      table or its partitions are excluded from the publication based on the
+      parameter <literal>publish_via_partition_root</literal>.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its
changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>

I felt that the second paragraph should be started with the sentence
"The partitioned table or its partitions are excluded...", so then
everything related to "publish_via_partition_root" is kept together.

~~~

8.
+  <para>
+   Create a publication that publishes all changes in all the tables except for
+   the changes of <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT users, departments;
+</programlisting>
+  </para>

The words "the changes of" are not needed, and you did not use that
wording in the ALTER PUBLICATION example.

======
doc/src/sgml/ref/psql-ref.sgml

9.
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables and schemas associated with each publication
are shown as
+        well.
</para>

/excluded tables and schemas/excluded tables, and schemas/

I addressed the remaining comments in the latest v26 patch [1]/messages/by-id/CANhcyEWGiWwGt1-v6d9bCAae9Np7cNPt+iRV9PXBZ0z=75XEVw@mail.gmail.com.
[1]: /messages/by-id/CANhcyEWGiWwGt1-v6d9bCAae9Np7cNPt+iRV9PXBZ0z=75XEVw@mail.gmail.com

Thanks,
Shlok Kyal

#137Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Shlok Kyal (#133)
3 attachment(s)
Re: Skipping schema changes in publication

On Tue, 11 Nov 2025 at 15:50, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Fri, 7 Nov 2025 at 11:36, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Some questions for the patch v25-0002 (EXCEPT tables)

======
doc/src/sgml/ref/alter_publication.sgml

1.
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable
class="parameter">exception_object</replaceable> [, ... ] ) ]

You can do both ADD/SET the <publication_object>, so really there
should be an ADD/SET ALL TABLES command as well, right?

These patches only added the ADD ALL TABLES command. I think once the
ADD ALL TABLES patch is committed, we can add the syntax SET ALL
TABLES.

~~~

2.
What was your reason for changing the syntax?
AFAICT those added "( )" are not strictly necessary, so I just
wondered your reason.

For example, we do not have any "( )" for <publication_object> [,...].
It is: ALTER PUBLICATION name ADD publication_object [, ...]
Not: ALTER PUBLICATION name ADD (publication_object [, ...])

So in the same way we could have EXCEPT syntax like that:
ALTER PUBLICATION name ADD ALL TABLES [EXCEPT <table_exception_object> [, ...]]
Where table_exception_object is: [ TABLE ] [ ONLY ] table_name [ * ]

Currently, if the user just wants to exclude a single table they must do:
ALTER PUBLICATION name ADD ALL TABLES EXCEPT (t1);
instead of just ALTER PUBLICATION name ADD ALL TABLES EXCEPT t1;

With recent commit now we support
CREATE PUBLICATION .. FOR ALL TABLES, ALL SEQUENCES.

Now when I am trying to support "FOR ALL TABLE EXCEPT t1, t2" , I am
getting a conflict when compiling this grammar.
For example
CREATE PUBLICATION .. FOR ALL TABLES EXCEPT t1, ...
After this comma, bison is giving conflict because it is not able to
figure whether to pick
ExceptPublicationObjSpec or a PublicationAllObjSpec.
So to handle this I introduced brackets around the table list.
And to make ALTER PUBLICATION similar to CREATE PUBLICATION, I have
added the same syntax for it.

So current syntax for CREATE/ALTER PUBLICATION is like:
CREATE PUBLICATION ... ALL TABLES EXCEPT TABLE(t1, t2, t3);
ALTER PUBLICATION ... ADD ALL TABLES EXCEPT TABLE(t1, t2, t3);

~~~

3.
BTW, I think you may need to consider a <table_exception_object>
instead of a generic name like <exception_object>, because in the
future if we EXCEPT SEQUENCES the <exception_object> name may be not
appropriate because things like [ONLY] and [*] are not applicable for
sequences.

Fixed

I have attached the latest patch here.
I have also addressed the comments for [1], [2].

[1]: /messages/by-id/CALDaNm0xDv96F+5LzcJYV6RC3Jg+RtdUqpQ-zoauwq3woTFzmQ@mail.gmail.com
[2]: /messages/by-id/CAHut+PsRD8ybC7MDBNBXXs=J2DuGiOc8kSePRyZc0s63U5f7tw@mail.gmail.com

The patches needed a rebase. Here are the rebased patches.

Thanks,
Shlok Kyal

Attachments:

v27-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v27-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 05de889a73305771934c5fdb3e56d30d036bf8ef Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 30 Oct 2025 10:52:56 +0530
Subject: [PATCH v27 1/3] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state. This includes resetting the publication
parameters, setting ALL TABLES and ALL SEQUENCES flags to false and dropping
the relations and schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  35 +++++--
 src/backend/commands/publicationcmds.c    | 108 ++++++++++++++++++--
 src/backend/parser/gram.y                 |  13 ++-
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/catalog/pg_publication.h      |  10 ++
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out | 119 ++++++++++++++++++++++
 src/test/regress/sql/publication.sql      |  53 ++++++++++
 8 files changed, 325 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 8dd250d2f3b..d8c24efd787 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -73,18 +74,32 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> and <literal>ALL SEQUENCES</literal> flags to
+   <literal>false</literal>, and removing all associated tables and schemas from
+   the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
    or <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
@@ -236,6 +251,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1faf3a8c372..50239513e3f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -90,12 +90,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1209,6 +1209,100 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES flag to false and drop
+ * all relations and schemas that are associated with the publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
+	replaces[Anum_pg_publication_puballsequences - 1] = true;
+
+	if (pubform->puballtables)
+		CacheInvalidateRelcacheAll();
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1553,6 +1647,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_Reset)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3a0a354a9c..a8b9ae6182d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10904,15 +10904,17 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *
  * ALTER PUBLICATION name ADD pub_obj [, ...]
  *
- * ALTER PUBLICATION name DROP pub_obj [, ...]
- *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name DROP pub_obj [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  *****************************************************************************/
 
 AlterPublicationStmt:
@@ -10954,6 +10956,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_Reset;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 51806597037..5d918abaa87 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2289,7 +2289,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..77b0a2f9eb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -152,6 +152,16 @@ extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
 
+/* default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
  * which allows callers to specify which partitions of partitioned tables
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..8cf75724a7b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4326,6 +4326,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_Reset,					/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..e3be29e378d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2009,6 +2009,125 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that associated tables are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.tbl1"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that associated schemas are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that 'PUBLISH' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | f       | f       | f       | f         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | t
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | stored            | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..84deaaf5a1f 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1268,6 +1268,59 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES;
+RESET client_min_messages;
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+
+-- Verify that 'ALL TABLES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that associated tables are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that associated schemas are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that 'PUBLISH' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that 'PUBLISH_GENERATED_COLUMNS' parameter is reset
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
-- 
2.34.1

v27-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v27-0003-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 5c2df3299f68d13666637dd071f66244013776b8 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 13 Nov 2025 11:09:01 +0530
Subject: [PATCH v27 3/3] Skip publishing the columns specified in FOR TABLE 
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/038_rep_changes_except_collist.pl       | 193 +++++++++++++
 19 files changed, 907 insertions(+), 217 deletions(-)
 create mode 100644 src/test/subscription/t/038_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a4d32de58ec..70144b67213 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6586,7 +6586,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c27d7462efd..17afcd79107 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1376,10 +1376,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1393,8 +1393,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1406,6 +1409,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1427,11 +1438,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1476,18 +1490,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <structname>t1</structname> to be used in the following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <para>
     Create a publication <literal>p1</literal>. A column list is defined for
-    table <structname>t1</structname> to reduce the number of columns that will be
-    replicated. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the order of column
+    names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1501,6 +1518,7 @@ Publications:
  postgres | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1521,23 +1539,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <structname>t1</structname> which now
-     only needs a subset of the columns that were on the publisher table
-     <structname>t1</structname>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <structname>t1</structname>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1549,11 +1585,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1562,6 +1608,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1658,6 +1711,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 8c3d219b9ea..885eb471cae 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">publication_drop_object</replaceable> is one of:</phrase>
@@ -267,6 +267,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departmen
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 8b616651272..9f581e96440 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -27,7 +27,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
@@ -96,17 +96,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -367,10 +374,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -393,6 +402,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -514,6 +533,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all sequences for synchronization:
 <programlisting>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index bec3a34e48f..02a1203dcad 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -266,14 +266,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -299,6 +304,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -660,10 +675,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -686,6 +703,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -790,8 +810,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -831,10 +853,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1291,6 +1315,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1315,11 +1340,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1331,8 +1374,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1363,6 +1410,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1371,6 +1425,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6fb69e3f3ba..04e75c5ef1c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -227,7 +227,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -381,8 +380,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -404,6 +403,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -427,7 +427,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -517,8 +518,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1500,6 +1507,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1513,23 +1521,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1556,13 +1569,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f2970cc3fdf..4314a4580e0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4480,6 +4480,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10796,14 +10801,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10819,7 +10825,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10827,7 +10833,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10837,8 +10843,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10846,25 +10853,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19856,6 +19865,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index dcc6124cc73..29a453d5f63 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -720,10 +720,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -737,6 +745,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -787,7 +798,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -801,7 +812,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -811,7 +832,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -858,6 +879,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -920,7 +944,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1052,11 +1077,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a9593c5d9da..7f534618cf4 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1091,12 +1101,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1145,19 +1164,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1169,7 +1210,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1179,9 +1220,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1505,6 +1548,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2078,6 +2128,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2127,6 +2178,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e34aaba7937..1fdb90f6482 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4933,24 +4933,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4976,10 +4959,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -5059,7 +5061,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 723b5575c53..ca2d356f72a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -690,6 +690,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 50b1d435359..6ceb108a35b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3053,16 +3138,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3072,35 +3168,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3121,34 +3249,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6532,49 +6634,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6764,6 +6823,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6772,11 +6837,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6788,8 +6850,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6799,14 +6861,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b376c400c69..3ce4bd517ca 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2295,6 +2295,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT (");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT"))
+		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -2315,10 +2319,13 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "WHERE", "("))
 		COMPLETE_WITH_ATTR(prev3_wd);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT", "("))
+		COMPLETE_WITH_ATTR(prev3_wd);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 !TailMatches("WHERE", "(*)"))
+			 !TailMatches("WHERE", "(*)") && !TailMatches("EXCEPT", "("))
 		COMPLETE_WITH(",", "WHERE (");
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 !ends_with(prev_wd, '('))
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
@@ -3636,7 +3643,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 0d39cb67779..594a2e14676 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -198,7 +198,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -208,6 +209,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 55cc7d5ee71..d38b0939c16 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2214,6 +2214,94 @@ ALTER PUBLICATION testpub_reset RESET;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index d3c03f54278..c917bb000aa 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1359,6 +1359,61 @@ ALTER PUBLICATION testpub_reset RESET;
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index b8e5c54c314..e8e69f7443d 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -47,6 +47,7 @@ tests += {
       't/035_conflicts.pl',
       't/036_sequences.pl',
       't/037_rep_changes_except_table.pl',
+      't/038_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/038_rep_changes_except_collist.pl b/src/test/subscription/t/038_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..3dfd266bc3d
--- /dev/null
+++ b/src/test/subscription/t/038_rep_changes_except_collist.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'Verify initial sync of tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'Verify initial sync of sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'Verify incremental inserts on tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||),
+	'Verify incremental inserts on sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v27-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v27-0002-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 447c15912c16e98fb3b67d088afbbc6ff05848f8 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 13 Nov 2025 10:56:20 +0530
Subject: [PATCH v27 2/3] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |  10 +-
 doc/src/sgml/ref/alter_publication.sgml       |  22 +-
 doc/src/sgml/ref/create_publication.sgml      |  47 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  99 +++++--
 src/backend/commands/publicationcmds.c        | 247 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  42 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +-
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 +++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  22 +-
 src/bin/psql/describe.c                       |  58 +++-
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   4 +
 src/test/regress/expected/publication.out     |  99 ++++++-
 src/test/regress/sql/publication.sql          |  52 +++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 186 +++++++++++++
 25 files changed, 910 insertions(+), 145 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..a4d32de58ec 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 79ecd09614f..c27d7462efd 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2547,10 +2547,12 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES or <literal>FOR TABLES IN SCHEMA</literal>, the
+   user must be a superuser. To add <literal>ALL TABLES</literal> or
+   <literal>TABLES IN SCHEMA</literal> to a publication, the user must be a
+   superuser. To add tables to a publication, the user must have ownership
+   rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index d8c24efd787..8c3d219b9ea 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
 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_drop_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
@@ -38,6 +39,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -93,8 +99,9 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   Adding or excluding a table from a publication requires ownership of that
+   table. The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
@@ -135,7 +142,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> does not have any effect.
      </para>
 
      <para>
@@ -244,6 +252,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departments);
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 66a70e5c5b5..8b616651272 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,8 +32,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -160,7 +164,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -180,6 +186,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>
+     <para>
+      The partitioned table or its partitions are excluded from the publication
+      based on the parameter <literal>publish_via_partition_root</literal>.
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -463,6 +498,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ac2f4ee3561..bec3a34e48f 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +479,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +506,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,9 +775,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -774,13 +801,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
  * should use GetAllPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +833,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -866,13 +897,19 @@ GetAllTablesPublications(void)
  * publication.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +928,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +950,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1207,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1178,7 +1218,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1408,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = (void *) sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 50239513e3f..6fb69e3f3ba 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,39 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +227,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +306,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +333,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -355,7 +394,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +418,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -515,7 +555,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -923,56 +963,54 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
-	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1041,7 +1079,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1161,7 +1199,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1263,6 +1301,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
 	replaces[Anum_pg_publication_puballsequences - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	if (pubform->puballtables)
 		CacheInvalidateRelcacheAll();
 
@@ -1285,7 +1344,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1303,6 +1365,80 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES ||
+		pubform->puballsequences != PUB_DEFAULT_ALL_SEQUENCES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1341,7 +1477,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1442,6 +1579,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1492,7 +1630,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1645,6 +1784,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_Reset)
@@ -1857,6 +2010,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1929,6 +2083,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 23ebaa3f230..55773cc2ecd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a8b9ae6182d..f2970cc3fdf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				except_pub_obj_list opt_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> ExceptPublicationObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10761,6 +10763,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10801,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10877,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = (List *) $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10897,6 +10904,28 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_except_clause:
+			EXCEPT opt_table '(' except_pub_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list: ExceptPublicationObjSpec
+					{ $$ = list_make1($1); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
@@ -10913,6 +10942,8 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] (table_name [, ...])
+ *
  * ALTER PUBLICATION name RESET
  *
  *****************************************************************************/
@@ -10956,6 +10987,15 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES opt_except_clause
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->pubobjects = (List *) $7;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RESET
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 942e1abdb58..a9593c5d9da 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2195,22 +2196,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2214,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2230,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2310,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc9084..96dd0ccf41a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a00918bacb4..e34aaba7937 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4662,7 +4664,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4831,6 +4860,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4842,8 +4872,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4854,6 +4892,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4869,6 +4908,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4880,6 +4920,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4893,7 +4934,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4934,6 +4979,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11812,6 +11860,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20182,6 +20233,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..f3c30f3be37 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -443,6 +445,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1715,6 +1728,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 445a541abf6..156319b8038 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
@@ -5157,7 +5177,7 @@ foreach my $run (sort keys %pgdump_runs)
 		#
 		# Either "all_runs" should be set or there should be a "like" list,
 		# even if it is empty.  (This makes the test more self-documenting.)
-		if (!defined($tests{$test}->{all_runs})
+		if (   !defined($tests{$test}->{all_runs})
 			&& !defined($tests{$test}->{like}))
 		{
 			die "missing \"like\" in test \"$test\"";
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 5d918abaa87..b376c400c69 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2292,11 +2292,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 ends_with(prev_wd, ','))
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
@@ -3623,6 +3628,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 77b0a2f9eb8..0d39cb67779 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /* default values for flags and publication parameters */
 #define PUB_DEFAULT_ACTION_INSERT true
@@ -178,9 +179,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -191,7 +193,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8cf75724a7b..a14ecedb27f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
@@ -4342,6 +4345,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e3be29e378d..55cc7d5ee71 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
@@ -2012,6 +2053,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
@@ -2020,8 +2062,18 @@ ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
 ERROR:  must be superuser to RESET publication
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
 SET ROLE regress_publication_user;
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
                                                    Publication testpub_reset
@@ -2038,8 +2090,25 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that associated tables are removed from the publication after RESET
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
+ALTER PUBLICATION testpub_reset RESET;
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2056,8 +2125,13 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that associated schemas are removed from the publication after RESET
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that associated schemas are removed from the publication after RESET
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2074,8 +2148,14 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that 'PUBLISH' parameter is reset
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2091,8 +2171,14 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
--- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
                                                    Publication testpub_reset
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
@@ -2127,6 +2213,7 @@ ALTER PUBLICATION testpub_reset RESET;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 84deaaf5a1f..d3c03f54278 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
@@ -1271,6 +1284,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES;
 RESET client_min_messages;
@@ -1279,34 +1293,59 @@ RESET client_min_messages;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
 SET ROLE regress_publication_user2;
 ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
 SET ROLE regress_publication_user;
 
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
 -- Verify that 'ALL TABLES' flag is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that associated tables are removed from the publication after RESET
+-- Should work now after resetting the publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can't add EXCEPT TABLE to 'FOR TABLE' publication
 ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that associated tables are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that associated schemas are removed from the publication after RESET
+-- Can't add EXCEPT TABLE to 'FOR ALL TABLES IN SCHEMA' publication
 ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that associated schemas are removed from the publication after RESET
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that 'PUBLISH' parameter is reset
+-- Can't add EXCEPT TABLE when the 'PUBLISH' parameter does not have default
+-- value
 ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that 'PUBLISH' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
--- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
+-- Can't add EXCEPT TABLE when 'PUBLISH_VIA_PARTITION_ROOT' parameter does not
+-- have default value
 ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1);
+
+-- Verify that 'PUBLISH_VIA_PARTITION_ROOT' parameter is reset
 \dRp+ testpub_reset
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
@@ -1319,6 +1358,7 @@ ALTER PUBLICATION testpub_reset RESET;
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..096e0606365
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE (sch1.tab1, public.tab1);
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.t1);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.part1);
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#138Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#137)
Re: Skipping schema changes in publication

Hi Shlok.

Some review comments for patch v27-0001.

======
doc/src/sgml/ref/alter_publication.sgml

1.
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to
the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> and <literal>ALL SEQUENCES</literal> flags to
+   <literal>false</literal>, and removing all associated tables and
schemas from
+   the publication.
   </para>

It would be better to give references to the actual
pg_publication.puballtables and .puballsequences flag fields [1]https://www.postgresql.org/docs/devel/catalog-pg-publication.html
instead of vaguely calling them the "<literal>ALL TABLES</literal> and
<literal>ALL SEQUENCES</literal> flags".

======
src/backend/commands/publicationcmds.c

AlterPublicationReset:

2.
+ if (pubform->puballtables)
+ CacheInvalidateRelcacheAll();

Does that also need to check ->puballsequences?

======
src/test/regress/sql/publication.sql

3.
If you want to, you can easily combine many of these test cases and
verify them in one go instead of separate ALTER/RESET for every kind
of flag.

~~~

4.
+-- Verify that 'ALL TABLES' flag is reset

Missing test to check the 'ALL SEQUENCES' flag gets reset?

======
[1]: https://www.postgresql.org/docs/devel/catalog-pg-publication.html

Kind Regards,
Peter Smith.
Fujitsu Australia.

#139Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#134)
Re: Skipping schema changes in publication

On Tue, Nov 11, 2025 at 9:22 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Fri, 7 Nov 2025 at 09:34, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

This is a general comment about the content of these patches.

IIUC, the v25* patches currently are currently arranged like this:

0001
- New command ALTER PUBLICATION pubname RESET;
0002
- Add new command: ALTER PUBLICATION pub_name ADD ALL TABLES;
- Enhance existing CREATE and the new ALTER syntax for EXCEPT tables
0003
- Enhance existing CREATE and ALTER syntax for EXCEPT col_list

~~~

IMO it is a bug that the ALTER PUBLICATION pub_name ADD/SET ALL TABLES
command does not already exist as a supported command. And, that is
independent of anything else you are implementing here like RESET or
EXCEPT.

Therefore, I think that one should be 1st in your patchset; The EXCEPT
stuff then just becomes enhancements to existing syntax, which would
give a cleaner separation of logic.

So, I am suggesting there should be 4 patches instead of 3. e.g.

SUGGESTION
0001 - New command: ALTER PUBLICATION pub_name ADD/SET ALL TABLES;
0002 - New command: ALTER PUBLICATION pubname RESET;
0003 - Enhance existing CREATE/ALTER syntax for EXCEPT tables
0004 - Enhance existing CREATE/ALTER syntax for EXCEPT col_list

I read the previous conversation in the thread. And got an
understanding that RESET was introduced so that we can have a way to
remove 'EXCEPT TABLE' from a publication and after RESET we can use
'ADD ALL TABLES [EXCEPT]' to alter the list of EXCEPT TABLE. So I
prefer to keep 'ALTER PUBLICATION .. RESET' as the first patch.
I think since 'ADD ALL TABLES' serves our current purpose. We can add
the syntax 'SET ALL TABLES' once 'ADD ALL TABLES' is in committed or
in committable shape.

Sure, you can defer the ALTER PUBLICATION ... SET ALL TABLES.

However, I still think that 'ALTER PUBLICATION ... ADD ALL TABLES' is
a self-contained new command that deserves to have its own *separate*
patch and tests and docs, etc.

IMO, patch 0002 is doing too much at once. It would be tidier (and
smaller and easier to review, etc) if you split 0002 to implement the
new 'ALTER PUBLICATION ... ADD ALL TABLES' separately, before
expanding on that to implement the EXCEPT part: 'ALTER PUBLICATION ...
ADD ALL TABLES [EXCEPT ...]'.

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

#140Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#137)
Re: Skipping schema changes in publication

Hi Shlok,

Here are a few ad-hoc comments for patch v27-0002.

======
doc/src/sgml/logical-replication.sgml

1.
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES or <literal>FOR TABLES IN SCHEMA</literal>, the
+   user must be a superuser. To add <literal>ALL TABLES</literal> or
+   <literal>TABLES IN SCHEMA</literal> to a publication, the user must be a
+   superuser. To add tables to a publication, the user must have ownership
+   rights on the table.
   </para>

Typo: Mismatched tags. "<literal>FOR ALL SEQUENCES or <literal>FOR
TABLES IN SCHEMA</literal>"

(The docs in v27-0002 cannot build because of this error)

======
src/backend/commands/publicationcmds.c

CheckPublicationDefValues:

2.
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)

Should a) also say "FOR ALL SEQUENCES"?

~~~

3.
What about checking defaults of other publication parameters, e.g.
publish_via_parttion_root and publish_generated_columns?

======
src/backend/parser/gram.y

4.
  * TABLE table_name [, ...]
  * TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name ADD ALL TABLES EXCEPT [TABLE] (table_name [, ...])
+ *
  * ALTER PUBLICATION name RESET

The EXCEPT clause part should be optional.

======
src/bin/pg_dump/t/002_pg_dump.pl

5.
  #
  # Either "all_runs" should be set or there should be a "like" list,
  # even if it is empty.  (This makes the test more self-documenting.)
- if (!defined($tests{$test}->{all_runs})
+ if (   !defined($tests{$test}->{all_runs})

Is this just an accidental whitespace change?

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

#141Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#137)
Re: Skipping schema changes in publication

Hi Shlok,

A comment about patch v27-0003

======
doc/src/sgml/logical-replication.sgml

All references to tables "t1" and "t2" should use the <structname> tag
in the SGML. The current patch has broken this in multiple places:

e.g.
-    Create a table <structname>t1</structname> to be used in the
following example.
+    Create tables <literal>t1</literal> and <literal>t2</literal> to be used in
+    the following example.

~~

e.g.
-    table <structname>t1</structname> to reduce the number of columns
that will be
-    replicated. Notice that the order of column names in the column list does
-    not matter.
+    table <literal>t1</literal>, and another column list is defined for table
+    <literal>t2</literal> using the <literal>EXCEPT</literal> clause to reduce
+    the number of columns that will be replicated. Note that the
order of column
+    names in the column lists does not matter.

~~

e.g.
-     On the subscriber node, create a table
<structname>t1</structname> which now
-     only needs a subset of the columns that were on the publisher table
-     <structname>t1</structname>, and also create the subscription
+     On the subscriber node, create tables <literal>t1</literal> and
+     <literal>t2</literal> which now only needs a subset of the columns that
+     were on the publisher tables <literal>t1</literal> and
+     <literal>t2</literal>, and also create the subscription

~~

e.g.
-     On the publisher node, insert some rows to table
<structname>t1</structname>.
+     On the publisher node, insert some rows to tables <literal>t1</literal>
+     and <literal>t2</literal>

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

#142Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#138)
4 attachment(s)
Re: Skipping schema changes in publication

On Fri, 14 Nov 2025 at 12:15, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Some review comments for patch v27-0001.

======
doc/src/sgml/ref/alter_publication.sgml

1.
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to
the default
+   state. This includes resetting all publication parameters, setting the
+   <literal>ALL TABLES</literal> and <literal>ALL SEQUENCES</literal> flags to
+   <literal>false</literal>, and removing all associated tables and
schemas from
+   the publication.
</para>

It would be better to give references to the actual
pg_publication.puballtables and .puballsequences flag fields [1]
instead of vaguely calling them the "<literal>ALL TABLES</literal> and
<literal>ALL SEQUENCES</literal> flags".

Fixed

======
src/backend/commands/publicationcmds.c

AlterPublicationReset:

2.
+ if (pubform->puballtables)
+ CacheInvalidateRelcacheAll();

Does that also need to check ->puballsequences?

I think we call CacheInvalidateRelcacheAll to invalide the relsync
cache for the case of ALTER Publication. For sequences we do not build
RelSyncEntry.
Also I see there are other similar occurrences (such as
RemovePublicationById, AlterPublicationOptions) where we do not
invalidate cache if we modify all sequence publications.
So, I think we do not require this check for puballsequences.

======
src/test/regress/sql/publication.sql

3.
If you want to, you can easily combine many of these test cases and
verify them in one go instead of separate ALTER/RESET for every kind
of flag.

~~~

I agree. I have made the changes in the latest patch.

4.
+-- Verify that 'ALL TABLES' flag is reset

Missing test to check the 'ALL SEQUENCES' flag gets reset?

Added the test.

======
[1] https://www.postgresql.org/docs/devel/catalog-pg-publication.html

I have also addressed the comments in [1]/messages/by-id/CAHut+PtRzCD4-0894cutkU_h8cPNtosN0_oSHn2iAKEfg2ENOQ@mail.gmail.com, [2]/messages/by-id/CAHut+PuHn-hohA4OdEJz+Zfukfr41TvMTeTH7NwJ=wg1+94uNA@mail.gmail.com.

[1]: /messages/by-id/CAHut+PtRzCD4-0894cutkU_h8cPNtosN0_oSHn2iAKEfg2ENOQ@mail.gmail.com
[2]: /messages/by-id/CAHut+PuHn-hohA4OdEJz+Zfukfr41TvMTeTH7NwJ=wg1+94uNA@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v28-0002-Support-ADD-ALL-TABLES-in-ALTER-PUBLICATION.patchapplication/octet-stream; name=v28-0002-Support-ADD-ALL-TABLES-in-ALTER-PUBLICATION.patchDownload
From b50f4b545d84363ca8645a1c15d9816ebec9a045 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 18 Nov 2025 20:13:06 +0530
Subject: [PATCH v28 2/4] Support ADD ALL TABLES in ALTER PUBLICATION

This patch adds support for using ADD ALL TABLES in ALTER PUBLICATION,
allowing an existing publication to be changed into an ALL TABLES
publication. This command is permitted only when the publication is
in its default state, meaning it has no tables or schemas added, its
ALL TABLES and ALL SEQUENCES flags are not set, and publication
options such as publish_via_root_partition, publish_generated_columns,
and publish are at their default values.
Usage:
ALTER PUBLICATION pub1 ADD ALL TABLES
---
 doc/src/sgml/logical-replication.sgml     | 10 ++-
 doc/src/sgml/ref/alter_publication.sgml   |  4 +-
 src/backend/commands/publicationcmds.c    | 89 +++++++++++++++++++++++
 src/backend/parser/gram.y                 | 10 +++
 src/bin/psql/tab-complete.in.c            |  2 +-
 src/include/nodes/parsenodes.h            |  1 +
 src/test/regress/expected/publication.out | 68 +++++++++++++++++
 src/test/regress/sql/publication.sql      | 47 ++++++++++++
 8 files changed, 225 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index aa013f348d4..c420469feaa 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2550,10 +2550,12 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index ccb276eb21d..9696b8880d7 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES
 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_drop_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
@@ -96,7 +97,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 70d34c77f4d..a699ba9dcb0 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1304,6 +1304,81 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check if the publication has default values.
+ *
+ * Returns true if the publication satisfies all the following conditions:
+ * a) Publication is not set with "FOR ALL TABLES" or "FOR ALL SEQUENCES"
+ * b) Publication is having default publication parameter values
+ * c) Publication is not associated with schemas
+ * d) Publication is not associated with relations
+ */
+static bool
+CheckPublicationDefValues(HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *pubobjs = NIL;
+
+	if (pubform->puballtables != PUB_DEFAULT_ALL_TABLES ||
+		pubform->puballsequences != PUB_DEFAULT_ALL_SEQUENCES)
+		return false;
+
+	if (pubform->pubinsert != PUB_DEFAULT_ACTION_INSERT ||
+		pubform->pubupdate != PUB_DEFAULT_ACTION_UPDATE ||
+		pubform->pubdelete != PUB_DEFAULT_ACTION_DELETE ||
+		pubform->pubtruncate != PUB_DEFAULT_ACTION_TRUNCATE ||
+		pubform->pubviaroot != PUB_DEFAULT_VIA_ROOT ||
+		pubform->pubgencols != PUB_DEFAULT_GENCOLS)
+		return false;
+
+	pubobjs = GetPublicationSchemas(pubid);
+	if (list_length(pubobjs))
+		return false;
+
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		return false;
+
+	return true;
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform PG_USED_FOR_ASSERTS_ONLY = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+#ifdef USE_ASSERT_CHECKING
+	Assert(!pubform->puballtables);
+#endif
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1646,6 +1721,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+	{
+		bool		isdefault = CheckPublicationDefValues(tup);
+
+		if (!isdefault)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					errmsg("adding ALL TABLES requires the publication to have default publication parameter values"),
+					errdetail("ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated."),
+					errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+		AlterPublicationSetAllTables(rel, tup);
+	}
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_Reset)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a8b9ae6182d..9d648ccb47b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10913,6 +10913,8 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name ADD ALL TABLES
+ *
  * ALTER PUBLICATION name RESET
  *
  *****************************************************************************/
@@ -10956,6 +10958,14 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RESET
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 5d918abaa87..0e11b1ba44c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2292,7 +2292,7 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8cf75724a7b..c22d75e80a2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4342,6 +4342,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4f20a911348..a907dcbb8f6 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2063,6 +2063,74 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- Tests for ALTER PUBLICATION ... ADD ALL TABLES
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; -- fail - must be superuser
+ERROR:  must be superuser to ADD ALL TABLES to the publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+-- Can't add ALL TABLES to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+ALTER PUBLICATION testpub_reset RESET;
+-- Can't add ALL TABLES to 'TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+ALTER PUBLICATION testpub_reset RESET;
+-- Can't add ALL TABLES when the 'PUBLISH' parameter does not have default value
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+ALTER PUBLICATION testpub_reset RESET;
+-- Can't add ALL TABLES when the 'PUBLISH_VIA_PARTITION_ROOT' parameter does
+-- not have default value
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+ALTER PUBLICATION testpub_reset RESET;
+-- Can't add ALL TABLES when the 'PUBLISH_GENERATED_COLUMNS' parameter does
+-- not have default value
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+DROP PUBLICATION testpub_reset;
+-- Can't add ALL TABLES to 'ALL SEQUENCES' publication
+CREATE PUBLICATION testpub_reset FOR ALL SEQUENCES;
+WARNING:  "wal_level" is insufficient to publish logical changes
+HINT:  Set "wal_level" to "logical" before creating subscriptions.
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+ALTER PUBLICATION testpub_reset RESET;
+-- Can add ALL TABLES to publication where all publication parameters are default
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Can't add ALL TABLES to 'ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
+DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
+HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP SCHEMA pub_sch1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 7a523ad067c..b73801932c3 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1298,6 +1298,53 @@ ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- Tests for ALTER PUBLICATION ... ADD ALL TABLES
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; -- fail - must be superuser
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+
+-- Can't add ALL TABLES to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can't add ALL TABLES to 'TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can't add ALL TABLES when the 'PUBLISH' parameter does not have default value
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can't add ALL TABLES when the 'PUBLISH_VIA_PARTITION_ROOT' parameter does
+-- not have default value
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can't add ALL TABLES when the 'PUBLISH_GENERATED_COLUMNS' parameter does
+-- not have default value
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+DROP PUBLICATION testpub_reset;
+
+-- Can't add ALL TABLES to 'ALL SEQUENCES' publication
+CREATE PUBLICATION testpub_reset FOR ALL SEQUENCES;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Can add ALL TABLES to publication where all publication parameters are default
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+
+-- Can't add ALL TABLES to 'ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP SCHEMA pub_sch1;
-- 
2.34.1

v28-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v28-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 0a02f5dc491d2ee1eaf0dbb7d41621d3265242b9 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 30 Oct 2025 10:52:56 +0530
Subject: [PATCH v28 1/4] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state. This includes resetting the publication
parameters, setting ALL TABLES and ALL SEQUENCES flags to false and dropping
the relations and schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   |  37 ++++++--
 src/backend/commands/publicationcmds.c    | 109 ++++++++++++++++++++--
 src/backend/parser/gram.y                 |  13 ++-
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/catalog/pg_publication.h      |  10 ++
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out |  57 +++++++++++
 src/test/regress/sql/publication.sql      |  34 +++++++
 8 files changed, 247 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 8dd250d2f3b..ccb276eb21d 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -73,18 +74,34 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
   </para>
 
   <para>
-   The remaining variants change the owner and the name of the publication.
+   The <literal>OWNER</literal> clause will change the owner of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RENAME</literal> clause will change the name of the
+   publication.
+  </para>
+
+  <para>
+   The <literal>RESET</literal> clause will reset the publication to the default
+   state. This includes resetting all publication parameters, setting the
+   <link linkend="catalog-pg-publication"><structname>pg_publication</structname></link>.<structfield>puballtables</structfield>
+   and
+   <link linkend="catalog-pg-publication"><structname>pg_publication</structname></link>.<structfield>puballsequences</structfield>
+   to <literal>false</literal>, and removing all tables and schemas that were
+   explicitly added to the publication.
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
    or <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
@@ -236,6 +253,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1faf3a8c372..70d34c77f4d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -90,12 +90,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1209,6 +1209,101 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES and ALL SEQUENCES flag
+ * to false and drop all relations and schemas that are associated with the
+ * publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
+	replaces[Anum_pg_publication_puballsequences - 1] = true;
+
+	if (pubform->puballtables)
+		CacheInvalidateRelcacheAll();
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1553,6 +1648,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_Reset)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3a0a354a9c..a8b9ae6182d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10904,15 +10904,17 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *
  * ALTER PUBLICATION name ADD pub_obj [, ...]
  *
- * ALTER PUBLICATION name DROP pub_obj [, ...]
- *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name DROP pub_obj [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  *****************************************************************************/
 
 AlterPublicationStmt:
@@ -10954,6 +10956,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_Reset;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 51806597037..5d918abaa87 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2289,7 +2289,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..77b0a2f9eb8 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -152,6 +152,16 @@ extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
 
+/* default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
  * which allows callers to specify which partitions of partitioned tables
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..8cf75724a7b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4326,6 +4326,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_Reset,					/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..4f20a911348 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2009,6 +2009,63 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
+RESET client_min_messages;
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+-- Verify that 'ALL TABLES', 'ALL SEQUENCES' flag is reset
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t             | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that associated tables, schemas and the publication parameters
+-- 'PUBLISH', 'PUBLISH_VIA_PARTITION_ROOT', and 'PUBLISH_GENERATED_COLUMNS'
+-- are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | f       | f       | f       | f         | stored            | t
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..7a523ad067c 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1268,6 +1268,40 @@ DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
+RESET client_min_messages;
+
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+
+-- Verify that 'ALL TABLES', 'ALL SEQUENCES' flag is reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that associated tables, schemas and the publication parameters
+-- 'PUBLISH', 'PUBLISH_VIA_PARTITION_ROOT', and 'PUBLISH_GENERATED_COLUMNS'
+-- are removed from the publication after RESET
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset SET (PUBLISH_VIA_PARTITION_ROOT = 'true');
+ALTER PUBLICATION testpub_reset SET (PUBLISH = '');
+ALTER PUBLICATION testpub_reset SET (PUBLISH_GENERATED_COLUMNS = stored);
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+
 RESET client_min_messages;
 
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
-- 
2.34.1

v28-0003-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v28-0003-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 79910fa996987424dc6e90a161a0940755b73868 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 18 Nov 2025 22:17:12 +0530
Subject: [PATCH v28 3/4] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/ref/alter_publication.sgml       |  22 ++-
 doc/src/sgml/ref/create_publication.sgml      |  47 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  99 +++++++---
 src/backend/commands/publicationcmds.c        | 161 ++++++++++-----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  36 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 +++++-
 src/bin/psql/tab-complete.in.c                |  10 +
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  59 +++++-
 src/test/regress/sql/publication.sql          |  24 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 186 ++++++++++++++++++
 24 files changed, 744 insertions(+), 133 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..a4d32de58ec 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 9696b8880d7..25b436bd27b 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -22,7 +22,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
-ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
 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_drop_object</replaceable> [, ...]
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
@@ -39,6 +39,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
     TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -96,8 +101,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES</literal>,
+   Adding or excluding a table to a publication additionally requires owning
+   that table. The <literal>ADD ALL TABLES</literal>,
    <literal>ADD TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
@@ -139,7 +144,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> does not have any effect.
      </para>
 
      <para>
@@ -248,6 +254,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departments);
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 66a70e5c5b5..8b616651272 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,8 +32,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -160,7 +164,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -180,6 +186,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>
+     <para>
+      The partitioned table or its partitions are excluded from the publication
+      based on the parameter <literal>publish_via_partition_root</literal>.
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -463,6 +498,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ac2f4ee3561..bec3a34e48f 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +479,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +506,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,9 +775,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -774,13 +801,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
  * should use GetAllPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +833,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -866,13 +897,19 @@ GetAllTablesPublications(void)
  * publication.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +928,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +950,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1207,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1178,7 +1218,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1408,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = (void *) sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a699ba9dcb0..732bcb4161b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,39 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +227,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +306,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +333,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -355,7 +394,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +418,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -515,7 +555,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -923,56 +963,54 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
-	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1041,7 +1079,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1161,7 +1199,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1264,6 +1302,27 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
 	replaces[Anum_pg_publication_puballsequences - 1] = true;
 
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
 	if (pubform->puballtables)
 		CacheInvalidateRelcacheAll();
 
@@ -1286,7 +1345,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1336,7 +1398,7 @@ CheckPublicationDefValues(HeapTuple tup)
 	if (list_length(pubobjs))
 		return false;
 
-	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	pubobjs = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 	if (list_length(pubobjs))
 		return false;
 
@@ -1417,7 +1479,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1518,6 +1581,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1568,7 +1632,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1947,6 +2012,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -2019,6 +2085,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 23ebaa3f230..55773cc2ecd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9d648ccb47b..2ae51e5bfe1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				except_pub_obj_list opt_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> ExceptPublicationObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10761,6 +10763,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10801,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10877,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = (List *) $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10897,6 +10904,28 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_except_clause:
+			EXCEPT opt_table '(' except_pub_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list: ExceptPublicationObjSpec
+					{ $$ = list_make1($1); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
@@ -10913,7 +10942,7 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
- * ALTER PUBLICATION name ADD ALL TABLES
+ * ALTER PUBLICATION name ADD ALL TABLES [EXCEPT [TABLE] (table_name [, ...])]
  *
  * ALTER PUBLICATION name RESET
  *
@@ -10958,10 +10987,11 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
-			| ALTER PUBLICATION name ADD_P ALL TABLES
+			| ALTER PUBLICATION name ADD_P ALL TABLES opt_except_clause
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
+					n->pubobjects = (List *) $7;
 					n->for_all_tables = true;
 					n->action = AP_AddObjects;
 					$$ = (Node *)n;
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 942e1abdb58..a9593c5d9da 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2195,22 +2196,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2214,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2230,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2310,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc9084..96dd0ccf41a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a00918bacb4..e34aaba7937 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4662,7 +4664,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4831,6 +4860,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4842,8 +4872,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4854,6 +4892,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4869,6 +4908,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4880,6 +4920,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4893,7 +4934,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4934,6 +4979,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11812,6 +11860,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20182,6 +20233,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..f3c30f3be37 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -443,6 +445,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1715,6 +1728,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 445a541abf6..bbfaeabb893 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 0e11b1ba44c..3633243386e 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2293,11 +2293,17 @@ match_previous_words(int pattern_id,
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			 ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
 	 * "ALTER PUBLICATION <name> SET TABLE <name> WHERE (" - complete with
@@ -3623,6 +3629,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 77b0a2f9eb8..0d39cb67779 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /* default values for flags and publication parameters */
 #define PUB_DEFAULT_ACTION_INSERT true
@@ -178,9 +179,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -191,7 +193,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index c22d75e80a2..a14ecedb27f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index a907dcbb8f6..383e492f99b 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
@@ -2012,6 +2053,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
 RESET client_min_messages;
@@ -2131,8 +2173,21 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 ERROR:  adding ALL TABLES requires the publication to have default publication parameter values
 DETAIL:  ALL TABLES or ALL SEQUENCES flag should not be set and no tables/schemas should be associated.
 HINT:  Use ALTER PUBLICATION ... RESET to reset the publication
+ALTER PUBLICATION testpub_reset RESET;
+-- Verify adding EXCEPT TABLE
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index b73801932c3..1bc1b84182b 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
@@ -1271,6 +1284,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
 RESET client_min_messages;
@@ -1344,9 +1358,15 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 
 -- Can't add ALL TABLES to 'ALL TABLES' publication
 ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Verify adding EXCEPT TABLE
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
 
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..096e0606365
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE (sch1.tab1, public.tab1);
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.t1);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.part1);
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v28-0004-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v28-0004-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From 4a618537511363ad08b0d783a42432ae1fc58cca Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 13 Nov 2025 11:09:01 +0530
Subject: [PATCH v28 4/4] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  55 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/038_rep_changes_except_collist.pl       | 193 +++++++++++++
 19 files changed, 907 insertions(+), 217 deletions(-)
 create mode 100644 src/test/subscription/t/038_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a4d32de58ec..70144b67213 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6586,7 +6586,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c420469feaa..1496e1c28ad 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1379,10 +1379,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1396,8 +1396,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1409,6 +1412,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1430,11 +1441,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1479,18 +1493,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <structname>t1</structname> to be used in the following example.
+    Create tables <structname>t1</structname> and <structname>t2</structname> to
+    be used in the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <para>
     Create a publication <literal>p1</literal>. A column list is defined for
-    table <structname>t1</structname> to reduce the number of columns that will be
-    replicated. Notice that the order of column names in the column list does
-    not matter.
+    table <structname>t1</structname>, and another column list is defined for
+    table <structname>t2</structname> using the <literal>EXCEPT</literal> clause
+    to reduce the number of columns that will be replicated. Note that the order
+    of column names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1504,6 +1521,7 @@ Publications:
  postgres | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1524,23 +1542,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <structname>t1</structname> which now
-     only needs a subset of the columns that were on the publisher table
-     <structname>t1</structname>, and also create the subscription
+     On the subscriber node, create tables <structname>t1</structname> and
+     <structname>t2</structname> which now only needs a subset of the columns
+     that were on the publisher tables <structname>t1</structname> and
+     <structname>t2</structname>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <structname>t1</structname>.
+     On the publisher node, insert some rows to tables <structname>t1</structname>
+     and <structname>t2</structname>.
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1552,11 +1588,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1565,6 +1611,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1661,6 +1714,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 25b436bd27b..7a0df1ba72c 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -32,7 +32,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">publication_drop_object</replaceable> is one of:</phrase>
@@ -269,6 +269,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departmen
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 8b616651272..9f581e96440 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -27,7 +27,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
-    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
+    TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( column_name [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ] [, ... ]
     TABLES IN SCHEMA { <replaceable class="parameter">schema_name</replaceable> | CURRENT_SCHEMA } [, ... ]
 
 <phrase>where <replaceable class="parameter">all_publication_object</replaceable> is one of:</phrase>
@@ -96,17 +96,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -367,10 +374,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -393,6 +402,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -514,6 +533,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all sequences for synchronization:
 <programlisting>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index bec3a34e48f..02a1203dcad 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -266,14 +266,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -299,6 +304,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -660,10 +675,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -686,6 +703,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -790,8 +810,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -831,10 +853,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1291,6 +1315,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1315,11 +1340,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1331,8 +1374,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1363,6 +1410,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1371,6 +1425,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 732bcb4161b..dedb846343a 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -227,7 +227,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -381,8 +380,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -404,6 +403,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -427,7 +427,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -517,8 +518,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1502,6 +1509,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1515,23 +1523,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1558,13 +1571,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2ae51e5bfe1..774dfebdfa5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4480,6 +4480,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10796,14 +10801,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10819,7 +10825,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10827,7 +10833,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10837,8 +10843,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10846,25 +10853,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19856,6 +19865,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index dcc6124cc73..29a453d5f63 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -720,10 +720,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -737,6 +745,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -787,7 +798,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -801,7 +812,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -811,7 +832,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -858,6 +879,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -920,7 +944,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1052,11 +1077,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a9593c5d9da..7f534618cf4 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1091,12 +1101,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1145,19 +1164,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1169,7 +1210,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1179,9 +1220,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1505,6 +1548,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2078,6 +2128,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2127,6 +2178,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e34aaba7937..1fdb90f6482 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4933,24 +4933,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4976,10 +4959,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -5059,7 +5061,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 723b5575c53..ca2d356f72a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -690,6 +690,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 50b1d435359..6ceb108a35b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3053,16 +3138,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3072,35 +3168,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3121,34 +3249,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6532,49 +6634,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6764,6 +6823,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6772,11 +6837,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6788,8 +6850,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6799,14 +6861,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 3633243386e..1e35ea409c1 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2295,6 +2295,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT (");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT"))
+		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -2316,10 +2320,13 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "WHERE", "("))
 		COMPLETE_WITH_ATTR(prev3_wd);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT", "("))
+		COMPLETE_WITH_ATTR(prev3_wd);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 !TailMatches("WHERE", "(*)"))
+			 !TailMatches("WHERE", "(*)") && !TailMatches("EXCEPT", "("))
 		COMPLETE_WITH(",", "WHERE (");
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 !ends_with(prev_wd, '('))
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
@@ -3637,7 +3644,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 0d39cb67779..594a2e14676 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -198,7 +198,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -208,6 +209,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 383e492f99b..e9b7fa0e54e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2188,6 +2188,94 @@ Except tables:
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 RESET client_min_messages;
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 1bc1b84182b..97f34596dcb 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1367,6 +1367,61 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 
 RESET client_min_messages;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index b8e5c54c314..e8e69f7443d 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -47,6 +47,7 @@ tests += {
       't/035_conflicts.pl',
       't/036_sequences.pl',
       't/037_rep_changes_except_table.pl',
+      't/038_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/038_rep_changes_except_collist.pl b/src/test/subscription/t/038_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..3dfd266bc3d
--- /dev/null
+++ b/src/test/subscription/t/038_rep_changes_except_collist.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'Verify initial sync of tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'Verify initial sync of sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'Verify incremental inserts on tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||),
+	'Verify incremental inserts on sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#143Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#139)
Re: Skipping schema changes in publication

On Mon, 17 Nov 2025 at 11:35, Peter Smith <smithpb2250@gmail.com> wrote:

On Tue, Nov 11, 2025 at 9:22 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Fri, 7 Nov 2025 at 09:34, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

This is a general comment about the content of these patches.

IIUC, the v25* patches currently are currently arranged like this:

0001
- New command ALTER PUBLICATION pubname RESET;
0002
- Add new command: ALTER PUBLICATION pub_name ADD ALL TABLES;
- Enhance existing CREATE and the new ALTER syntax for EXCEPT tables
0003
- Enhance existing CREATE and ALTER syntax for EXCEPT col_list

~~~

IMO it is a bug that the ALTER PUBLICATION pub_name ADD/SET ALL TABLES
command does not already exist as a supported command. And, that is
independent of anything else you are implementing here like RESET or
EXCEPT.

Therefore, I think that one should be 1st in your patchset; The EXCEPT
stuff then just becomes enhancements to existing syntax, which would
give a cleaner separation of logic.

So, I am suggesting there should be 4 patches instead of 3. e.g.

SUGGESTION
0001 - New command: ALTER PUBLICATION pub_name ADD/SET ALL TABLES;
0002 - New command: ALTER PUBLICATION pubname RESET;
0003 - Enhance existing CREATE/ALTER syntax for EXCEPT tables
0004 - Enhance existing CREATE/ALTER syntax for EXCEPT col_list

I read the previous conversation in the thread. And got an
understanding that RESET was introduced so that we can have a way to
remove 'EXCEPT TABLE' from a publication and after RESET we can use
'ADD ALL TABLES [EXCEPT]' to alter the list of EXCEPT TABLE. So I
prefer to keep 'ALTER PUBLICATION .. RESET' as the first patch.
I think since 'ADD ALL TABLES' serves our current purpose. We can add
the syntax 'SET ALL TABLES' once 'ADD ALL TABLES' is in committed or
in committable shape.

Sure, you can defer the ALTER PUBLICATION ... SET ALL TABLES.

However, I still think that 'ALTER PUBLICATION ... ADD ALL TABLES' is
a self-contained new command that deserves to have its own *separate*
patch and tests and docs, etc.

IMO, patch 0002 is doing too much at once. It would be tidier (and
smaller and easier to review, etc) if you split 0002 to implement the
new 'ALTER PUBLICATION ... ADD ALL TABLES' separately, before
expanding on that to implement the EXCEPT part: 'ALTER PUBLICATION ...
ADD ALL TABLES [EXCEPT ...]'.

I agree with you.
I have split the 0002 patch. Now we have following patches

0001 - Add RESET clause to Alter Publication
0002 - Support ADD ALL TABLES in ALTER PUBLICATION
0003 - Skip publishing the tables specified in EXCEPT TABLE
0004 - Skip publishing the columns specified in FOR TABLE EXCEPT

I have attached the updated patch in [1]/messages/by-id/CANhcyEXCKPCAdoqBLAhxt64Nwf+7T52dd8daE3qvhBNTvro13Q@mail.gmail.com.
[1]: /messages/by-id/CANhcyEXCKPCAdoqBLAhxt64Nwf+7T52dd8daE3qvhBNTvro13Q@mail.gmail.com

Thanks,
Shlok Kyal

#144Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#142)
Re: Skipping schema changes in publication

Hi Shlok,

Some review comments for patch v28-0001.

======
src/backend/commands/publicationcmds.c

AlterPublicationReset:
1.
+ values[Anum_pg_publication_puballtables - 1] =
BoolGetDatum(PUB_DEFAULT_ALL_TABLES);
+ replaces[Anum_pg_publication_puballtables - 1] = true;
+
+ values[Anum_pg_publication_puballsequences - 1] =
BoolGetDatum(PUB_DEFAULT_ALL_SEQUENCES);
+ replaces[Anum_pg_publication_puballsequences - 1] = true;

The PUB_DEFAULT_xxx made sense for all the publication parameters, but
these are just flags that say if the ALL TABLES or ALL SEQUENCES
clauses are present in the command, so I'm not sure that "default"
macros are appropriate here.

e.g. it seems better to reset those using hardwired booleans:

values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(false);
values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(false);

(This would also be consistent with what the function comment is saying)

======
src/include/catalog/pg_publication.h

2.
+/* default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE

As in the previous comment #1, I don't think we should define
PUB_DEFAULT_ALL_TABLES and PUB_DEFAULT_ALL_SEQUENCES. Also, change the
comment to remove the words "flags and".

======
src/test/regress/sql/publication.sql

3.
I don't think the parameter names should be written in uppercase
because that's not how they normally appear in the docs and examples.

~~~

4.
+-- Verify that 'ALL TABLES', 'ALL SEQUENCES' flag is reset

typo: /flag is reset/flags are reset/

~~~

5.
+-- Verify that associated tables, schemas and the publication parameters
+-- 'PUBLISH', 'PUBLISH_VIA_PARTITION_ROOT', and 'PUBLISH_GENERATED_COLUMNS'
+-- are removed from the publication after RESET

The comment makes it sound like parameters are "removed". Maybe some
rewording is needed.

SUGGESTION
Verify that a publication RESET removes the associated tables and
schemas, and sets default values for publication parameters 'publish',
'publish_via_partition_root', and 'publish_generated_columns'.

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

#145Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#142)
Re: Skipping schema changes in publication

Hi Shlok.

Thanks for splitting the patches.

Here are some review comments for the new patch v28-0002 (ADD ALL TABLES).

======
Commit Message

1.
This patch adds support for using ADD ALL TABLES in ALTER PUBLICATION,
allowing an existing publication to be changed into an ALL TABLES
publication. This command is permitted only when the publication is
in its default state, meaning it has no tables or schemas added, its
ALL TABLES and ALL SEQUENCES flags are not set, and publication
options such as publish_via_root_partition, publish_generated_columns,
and publish are at their default values.

~

IMO, the restrictions for this new command are too severe:

e.g. If I already have a FOR ALL SEQUENCES publication, then I
expected it should be possible to ADD ALL TABLES to that as well,
right?

Likewise, why are we enforcing that the publication parameters must be
defaults? IOW, why is (i) below disallowed, but (ii) is allowed?

(i)
ALTER PUBLICATION pub SET (publish_generated_columns=stored);
ALTER PUBLICATION pub ADD ALL TABLES;

(ii)
ALTER PUBLICATION pub ADD ALL TABLES;
ALTER PUBLICATION pub SET (publish_generated_columns=stored);

======
doc/src/sgml/ref/alter_publication.sgml

Description:

2.
The "Description" part of this page is confusing because it was
referring to "The first three variants" and later "The fourth
variant". Now that the "ADD ALL TABLES" variant has been added, I
have lost track of what "variants" this description is talking about.
Those words should be replaced by something clearer. This could be an
ongoing issue if it is not worded differently because the same problem
will happen again, e.g. when more syntax gets added for ALL SEQUENCES,
etc.

~~~

3.
Note also that DROP TABLES IN SCHEMA will not drop any schema tables
that were specified using FOR TABLE/ ADD TABLE.

~

That sentence (above) is from the docs. Does that also need updating
now that there is ADD ALL TABLES?

======
src/backend/commands/publicationcmds.c

CheckPublicationDefValues:

4.
Is this function needed?

~~~

AlterPublication:

5.
+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("adding ALL TABLES requires the publication to have default
publication parameter values"),
+ errdetail("ALL TABLES or ALL SEQUENCES flag should not be set and no
tables/schemas should be associated."),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+ AlterPublicationSetAllTables(rel, tup);
+ }
+

Why do we need this self-imposed restriction?

======
src/include/nodes/parsenodes.h

6.
List *pubobjects; /* Optional list of publication objects */
+ bool for_all_tables; /* Special publication for all tables in db */
AlterPublicationAction action; /* What action to perform with the given
* objects */
} AlterPublicationStmt;

There is no such "FOR" syntax like ALTER PUBLICATION ... FOR ALL
TABLES, so I felt just 'puballtables' might be a better member name.

======
src/test/regress/sql/publication.sql

7.
Don't uppercase any of the publication parameters because they never
appear in the docs/examples like that.

~

8.
So that the last command is the one being tested, I felt that all the
test cases should be doing RESET *first* instead of last.

~~~

9.
You don't always need to use RESET. There should also be some tests
using an "empty" publication just to be sure it works. e.g

CREATE PUBLICATION pub_empty;
ALTER PUBLICATION pub_empty ADD ALL TABLES;

~~~

10.
As commented earlier, I felt the rules were too restrictive. So I
think some test cases can be removed.

~~~

11.
+-- Tests for ALTER PUBLICATION ... ADD ALL TABLES

~

I noticed there is a "--
======================================================" separator
between the major groups of tests.

11a. Should use this separator in patch 0001 for the RESET group of tests

11b. Should use this separator in patch 0002 for the ADD ALL TABLES
groups of tests

~~~

12.
+-- Can't add ALL TABLES to 'ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+

This test case seems to belong earlier, near the 'FOR TABLE' and the
'TABLES IN SCHEMA' tests.

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

#146Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#142)
Re: Skipping schema changes in publication

Hi Shlok.

Here are some review comments for your patch v28-0003 (EXCEPT TABLE ...).

The review of this patch is a WIP. In this post I only looked at the test code.

======
.../t/037_rep_changes_except_table.pl

1.
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications

Use uppercase: /except table/EXCEPT TABLE/

~~~

2.
There are lots of test cases dedicated to partiion-table testing. I
felt a bigger comment separating these major groups might be helpful.

Something like:

-- ============================================
-- EXCEPT TABLE test cases for normal tables
-- ============================================

and

-- ============================================
-- EXCEPT TABLE test cases for partition tables
-- ============================================

~~~

3.
+# Initialize publisher node
...
+# Create subscriber node

Those 2 comments should be almost alike -- e.g. both should say
"Initialize" or both should say "Create".

~~~

4.
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE SCHEMA sch1;
+ CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+ CREATE TABLE public.tab1(a int);
+));
+

That first sentence ("Test replication with ...") is not needed here.
The is just repeating the purpose of the entire file, so that comment
can replace the one at the top of this file.

~~~

5.
+# Insert some data and verify that inserted data is not replicated

Be explicit that we are referring to the excluded table.

SUGGESTION (e.g.)
Verify that data inserted to the excluded table is not replcated.

~~~

6.
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ ALTER PUBLICATION tap_pub_schema RESET;
+ ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE
(sch1.tab1, public.tab1);
+ INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+

It is not strictly needed for these tests, but do you think it makes
more sense to also do an ALTER SUBSCRIPTION ... REFRESH PUBLICATION;
whenever you change the publications?

~~~

7.
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+

double-blank lines.

~~~

8.
I think it would be more helpful if the partition table test cases say
(in their comments) a lot more about the steps they are doing, and
what they expect the result to be. Sure, I can read all the code to
figure it out for each case, but it is better to know the test
intentions/expectations then verify they are doing the right thing.

~~~

9.
+ CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+ CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);

Maybe create this table to have *multiple* partitions. It might be
interesting later to see what happens when you try to EXCEPT only one
of the partitions.

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

#147Peter Smith
smithpb2250@gmail.com
In reply to: Peter Smith (#146)
Re: Skipping schema changes in publication

On Fri, Nov 21, 2025 at 5:55 PM Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Here are some review comments for your patch v28-0003 (EXCEPT TABLE ...).

The review of this patch is a WIP. In this post I only looked at the test code.

Here are my remaining review comments for patch v28-0003 (EXCEPT TABLE ...).

======
doc/src/sgml/ref/create_publication.sgml

1.
-ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable
class="parameter">table_exception_object</replaceable> [, ... ] ) ]

Why is that optional [TABLE] keyword needed?

I know PostGres commands sometimes have "noise" words in the syntax so
the command can be more English-like, but in this case, the
publication is a FOR ALL *TABLES* anyway, so I am not sure what the
benefit is of the user being able to say TABLE a 2nd time?

======
src/backend/catalog/pg_publication.c

2.
+ /*
+ * Check for partitions of partitioned table which are specified with
+ * EXCEPT clause and partitioned table is published with
+ * publish_via_partition_root = true.
+ */

I think you can just say "partitions" or "table partitions", but
"partitions of [a] partitioned table" seems overkill.

Also, "... and partitioned table is published with
publish_via_partition_root = true." seems too wordy. Isn't that just
the same as "... and publish_via_partition_root = true"

SUGGESTION
Check for when the publication says "EXCEPT TABLE (partition)" but
publish_via_partition_root = true.

~~~

3.
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
  List    *result = NIL;
  CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
  HeapTuple tup = &pubrellist->members[i]->tuple;
  Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
- result = lappend_oid(result, pubid);
+ if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+ result = lappend_oid(result, pubid);
  }

I was wondering if it might be better to return 2 lists from this
function (e.g. an included-list, and an excluded-list) instead of
passing the 'except_flag' like the current code. IIUC, you are mostly
calling this function twice to get 2 lists anyway, but returning 2
lists instead of 1, this function might be more efficient since it
will only process the publication loop once.

~~~

4.
/*
* Gets list of relation oids for a publication that matches the except_flag.
*
* This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
* should use GetAllPublicationRelations().
*/
List *
GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
bool except_flag)
Something doesn't seem right -- the function comment says we shouldn't
be calling the function for FOR ALL TABLES, but meanwhile, EXCEPT
TABLE is currently only implemented via FOR ALL TABLES. So it feels
contradictory. Maybe it is just the comment that needs updating?

~~~

5.
/*
* Gets list of relation oids for a publication that matches the except_flag.
*
* This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
* should use GetAllPublicationRelations().
*/
List *
GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
bool except_flag)
{
List *result;
Relation pubrelsrel;
ScanKeyData scankey;
SysScanDesc scan;
HeapTuple tup;

/* Find all publications associated with the relation. */
pubrelsrel = table_open(PublicationRelRelationId, AccessShareLock);

Existing bug? Isn't this a bogus comment?
/* Find all publications associated with the relation. */

Was that meant to be the other way around? -- e.g. Find all the
relations associated with the specified publication.

======
src/backend/commands/publicationcmds.c

6.
+ default:
+ /* shouldn't happen */
+ elog(ERROR, "invalid publication object type %d",
+ puballobj->pubobjtype);
+ break;

I think the ERROR is enough of a clue that it shouldn't happen. I felt
the comment was redundant.

~~~

ObjectsInPublicationToOids:

7.
  case PUBLICATIONOBJ_TABLE:
+ pubobj->pubtable->except = false;
+ *rels = lappend(*rels, pubobj->pubtable);
+ break;
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = true;
  *rels = lappend(*rels, pubobj->pubtable);
  break;
Those are very similar. How about combining like below?

case PUBLICATIONOBJ_TABLE:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = (pubobj->pubobjtype ==
PUBLICATIONOBJ_EXCEPT_TABLE);
*rels = lappend(*rels, pubobj->pubtable);
break;

~~

pub_contains_invalid_column:

8.
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  bool pubviaroot, char pubgencols_type,
- bool *invalid_column_list,
+ bool puballtables, bool *invalid_column_list,
  bool *invalid_gen_col)

The 'pub_via_root' and 'pubgencols_type' are parameters. Somehow it
seems more natural for the 'puballtables' to be passed before those,
because FOR ALL TABLES comes before WITH in the syntax.

~~~

CreatePublication:

9.
else if (!stmt->for_all_sequences)
- {
ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
&schemaidlist);

AFAICT, this function is refactored a lot because of the removal of
that '{'. It looks like mostly whitespace, but really, I think the
logic is quite different. I wasn't sure what that was about. Is it
related to this patch, or some other bugfix in passing or what?

======
src/backend/commands/tablecmds.c

ATPrepChangePersistence:

10.
- GetRelationPublications(RelationGetRelid(rel)) != NIL)
+ list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)

Isn't an empty List the same as a NIL list? Maybe that list_length()
change was not really needed.

======
src/backend/parser/gram.y

11.
drop_option_list pub_obj_list pub_all_obj_type_list
+ except_pub_obj_list opt_except_clause

Is this name consistent with the others? Should it be pub_except_obj_list?

~~~

12.
%type <publicationobjectspec> PublicationObjSpec
+%type <publicationobjectspec> ExceptPublicationObjSpec
%type <publicationallobjectspec> PublicationAllObjSpec

Is this name consistent with the others? Should it be PublicationExceptObjSpec?

~~~

CreatePublicationStmt:

13.
n->pubname = $3;
+ n->pubobjects = $5;

I noticed that sometimes there is a cast (List *) and other times
there is not. e.g. none here, but cast in AlterPublicationStmt. Why
the differences?

~~~

PublicationObjSpec:

14.
The comment for 'PublicationObjSpec' says "FOR TABLE and FOR TABLES IN
SCHEMA specifications". If that comment is correct, then why is this
patch changing this code? OTOH, if the code is correct, then does the
comment need updating?

======
src/bin/pg_dump/pg_dump.c

15.
Shouldn't there have already been some ALTER ... ADD ALL TABLE dump
code and test code implemented back in patch 0002?

~~~

dumpPublication:

16.
  else if (pubinfo->puballtables)
+ {
+ SimplePtrListCell *cell;
+
  appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+ /* Include exception tables if the publication has except tables */
+ for (cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == pubrinfo->publication)
+ {
+ tbinfo = pubrinfo->pubtable;
+
+ if (first)
+ {
+ appendPQExpBufferStr(query, " EXCEPT TABLE (");
+ first = false;
+ }
+ else
+ appendPQExpBufferStr(query, ", ");
+ appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+ }
+ }
+ if (!first)
+ appendPQExpBufferStr(query, ")");
+ }

16a.
SimplePtrListCell *cell can be declared as a for-loop variable.

~

16b.
The comment should say "EXCEPT TABLES" in uppercase.

~

16c.
I am not convinced you can use that 'first' flag like you are doing.
Isn't that interfering with the existing usage of that flag? Perhaps
another boolean just for this EXCEPT loop is needed.

~~~

getPublicationTables:

17.
+ if (strcmp(prexcept, "f") == 0)
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+ else
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+

...

+ if (strcmp(prexcept, "t") == 0)
+ simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+

Here you are comparing the same 'prexcept' flag for both "f" and "t".

I felt it was better if both comparisons are the same (e.g. both "t").

Or better still, assign a new boolean and avoid that 2nd strcmp
entirely -- e.g. except_flag = (strcmp(prexcept, "t") == 0);

======
src/bin/pg_dump/pg_dump_sort.c

DOTypeNameCompare:

18.
+ else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+ {
+ PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+ PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+ /* Sort by publication name, since (namespace, name) match the rel */
+ cmpval = strcmp(probj1->publication->dobj.name,
+ probj2->publication->dobj.name);
+ if (cmpval != 0)
+ return cmpval;
+ }

Isn't this identical to the previous code block? So can't you just add
DO_PUBLICATION_EXCEPT_REL to that condition?

======
src/bin/pg_dump/t/002_pg_dump.pl

19.
Missing test cases for ALTER? But also.

~~~

20.
Missing test cases for EXCEPT for INHERITED tables?

======
src/bin/psql/describe.c

describeOneTableDetails:

21.
I was wondering if the "describe" for tables (e.g. \d+) should also
show the publications where the table is an ECEPT TABLE? How else is
the user going to know it has been excluded by some publication?

======
src/bin/psql/tab-complete.in.c

ALTER PUBLICATION:

22.
The tab completion does not seem as good as it could be. e.g, there is
missing '(' and the for EXCEPT TABLE

~~~

CREATE PUBLICATION:

23.
The tab completion does not seem as good as it could be. e.g, there is
missing '(' and the for EXCEPT TABLE

======
src/test/regress/sql/publication.sql

24.
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1

As well as doing the "describes" for the publication, I think we need
to see the test cases for the describes of those excluded tables. e.g.
I imagine that they should also list the publications that they are
*excluded* from, right?

~~~

25.
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);

25a.
Needs some explanatory comments here saying these are for testing the
EXCEPT with inherited tables (e.g. ONLY versus not).

~

25b.
I think you should be testing the '*' syntax here too.

~~~

26.
+CREATE TABLE pub_sch1.tbl2 (a int);
SET client_min_messages = 'ERROR';
CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
RESET client_min_messages;
@@ -1344,9 +1358,15 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES;

 -- Can't add ALL TABLES to 'ALL TABLES' publication
 ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Verify adding EXCEPT TABLE
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE
(pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset

DROP PUBLICATION testpub_reset;
DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
DROP SCHEMA pub_sch1;

It looks like that added CREATE TABLE (and RESET?) belongs more
appropriately within the scope of the new test "Verify adding EXCEPT
TABLE".

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

#148Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#142)
Re: Skipping schema changes in publication

Hi Shlok.

I checked the latest v28-0004 "EXCEPT (col-list)" patch. I have no
code review comments, but I do have one syntax question.

======

The result of the current patch proposed syntax is like:

CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

~~

In the previous patch v28-0003 (FOR ALL TABLES EXCEPT [TABLE]), I
thought the optional noise-word TABLE did not have any user benefit
because TABLE was already obvious.

OTOH, here in patch v28-0004, it might be helpful to have an
*optional* [COLUMN] part. e.g., I felt "EXCEPT [COLUMN]" would
improve the readability of these commands.

Compare:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT COLUMN (col1, col2, col3)

Compare:
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT COLUMN (col1, col2, col3)

This is similar to the optional "[COLUMN]" keyword used here [1]https://www.postgresql.org/docs/devel/sql-altertable.html.

Thoughts?

======
[1]: https://www.postgresql.org/docs/devel/sql-altertable.html

Kind Regards,
Peter Smith.
Fujitsu Australia

#149shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#142)
Re: Skipping schema changes in publication

On Wed, Nov 19, 2025 at 3:34 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have also addressed the comments in [1], [2].

[1]: /messages/by-id/CAHut+PtRzCD4-0894cutkU_h8cPNtosN0_oSHn2iAKEfg2ENOQ@mail.gmail.com
[2]: /messages/by-id/CAHut+PuHn-hohA4OdEJz+Zfukfr41TvMTeTH7NwJ=wg1+94uNA@mail.gmail.com

Thanks for the patch. Please find a few comments on 001:

1)
+ bool nulls[Natts_pg_publication];
+ bool replaces[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));

Can we initialize all 3 like below? Then we do not need memset.
bool nulls[Natts_pg_publication] = {0};

2)
AlterPublicationReset():
Can we reset the columns in same sequence as they are originally
defined (see pg_publication_d.h), it makes it easy to map when
comparing/reviewing that we do not have missed any.

3)
+/* default values for flags and publication parameters */
...
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE

These too we can rearrange as per order in pg_publication_d.h

4)
+ COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");

Is it supposed to be in alphabetical order? Looking at others, I do
not think so. If not, then I feel 'SET' first followed by 'RESET'
seems a more obvious choice to me. See similar (ENABLE followed by
DISABLE):
COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO",

5)
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;

We can use 'DROP SCHEMA pub_sch1 CASCADE'. Then we need not to worry
about dropping the associated table(s) (even if we create more in
future in this schema).

thanks
Shveta

#150shveta malik
shveta.malik@gmail.com
In reply to: shveta malik (#149)
Re: Skipping schema changes in publication

On Wed, Dec 3, 2025 at 8:56 AM shveta malik <shveta.malik@gmail.com> wrote:

On Wed, Nov 19, 2025 at 3:34 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have also addressed the comments in [1], [2].

[1]: /messages/by-id/CAHut+PtRzCD4-0894cutkU_h8cPNtosN0_oSHn2iAKEfg2ENOQ@mail.gmail.com
[2]: /messages/by-id/CAHut+PuHn-hohA4OdEJz+Zfukfr41TvMTeTH7NwJ=wg1+94uNA@mail.gmail.com

Thanks for the patch. Please find a few comments on 001:

1)
+ bool nulls[Natts_pg_publication];
+ bool replaces[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));

Can we initialize all 3 like below? Then we do not need memset.
bool nulls[Natts_pg_publication] = {0};

2)
AlterPublicationReset():
Can we reset the columns in same sequence as they are originally
defined (see pg_publication_d.h), it makes it easy to map when
comparing/reviewing that we do not have missed any.

3)
+/* default values for flags and publication parameters */
...
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_ALL_TABLES false
+#define PUB_DEFAULT_ALL_SEQUENCES false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE

These too we can rearrange as per order in pg_publication_d.h

4)
+ COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");

Is it supposed to be in alphabetical order? Looking at others, I do
not think so. If not, then I feel 'SET' first followed by 'RESET'
seems a more obvious choice to me. See similar (ENABLE followed by
DISABLE):
COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO",

5)
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;

We can use 'DROP SCHEMA pub_sch1 CASCADE'. Then we need not to worry
about dropping the associated table(s) (even if we create more in
future in this schema).

6)
Just before AlterPublicationReset-->LockSchemaList does
SearchSysCacheExists1(), I dropped the schema concurrently, which
resulted in below error:

postgres=# alter publication pub1 reset;
ERROR: schema with OID 16393 does not exist

I do not think the user should be seeing this error. For
CreatePublication and AlterPublicationSchemas, it makes sense to give
an error when it calls LockSchemaList as user has given the schema
names himself. But here since schemas are internally fetched, I think
it will be logical to skip the
error if it gets dropped concurrently. Thoughts?

If we plan to skip error, we can do so by passing the flag
missing_okay=true. See RangeVarGetRelid and other such functions using
such a flag.

thanks
Shveta

#151Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#145)
4 attachment(s)
Re: Skipping schema changes in publication

On Thu, 20 Nov 2025 at 11:54, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Thanks for splitting the patches.

Here are some review comments for the new patch v28-0002 (ADD ALL TABLES).

======
Commit Message

1.
This patch adds support for using ADD ALL TABLES in ALTER PUBLICATION,
allowing an existing publication to be changed into an ALL TABLES
publication. This command is permitted only when the publication is
in its default state, meaning it has no tables or schemas added, its
ALL TABLES and ALL SEQUENCES flags are not set, and publication
options such as publish_via_root_partition, publish_generated_columns,
and publish are at their default values.

~

IMO, the restrictions for this new command are too severe:

e.g. If I already have a FOR ALL SEQUENCES publication, then I
expected it should be possible to ADD ALL TABLES to that as well,
right?

Likewise, why are we enforcing that the publication parameters must be
defaults? IOW, why is (i) below disallowed, but (ii) is allowed?

(i)
ALTER PUBLICATION pub SET (publish_generated_columns=stored);
ALTER PUBLICATION pub ADD ALL TABLES;

(ii)
ALTER PUBLICATION pub ADD ALL TABLES;
ALTER PUBLICATION pub SET (publish_generated_columns=stored);

I agree that the current restrictions were too strict. With the latest
patch we avoid adding ALL TABLES only when we have an existing list of
tables or schemas in a publication.

======
doc/src/sgml/ref/alter_publication.sgml

Description:

2.
The "Description" part of this page is confusing because it was
referring to "The first three variants" and later "The fourth
variant". Now that the "ADD ALL TABLES" variant has been added, I
have lost track of what "variants" this description is talking about.
Those words should be replaced by something clearer. This could be an
ongoing issue if it is not worded differently because the same problem
will happen again, e.g. when more syntax gets added for ALL SEQUENCES,
etc.

~~~

I have updated the description to avoid the wording "The first three
variants". Instead I have added a list to describe each command
separately. Similar to ALTER TABLE [1]https://www.postgresql.org/docs/current/sql-altertable.html.

3.
Note also that DROP TABLES IN SCHEMA will not drop any schema tables
that were specified using FOR TABLE/ ADD TABLE.

~

That sentence (above) is from the docs. Does that also need updating
now that there is ADD ALL TABLES?

When we create a publication on a schema, we can also add specific
tables using FOR TABLE/ADD TABLE.
But in case of ALL TABLES publication we are not allowed to include
tables using FOR TABLE/ADD TABLE.

So for ALL TABLES case this wording is not required.

======
src/backend/commands/publicationcmds.c

CheckPublicationDefValues:

4.
Is this function needed?

It is not needed. Modified the function to give proper error messages
for each case.

~~~

AlterPublication:

5.
+ if (stmt->for_all_tables)
+ {
+ bool isdefault = CheckPublicationDefValues(tup);
+
+ if (!isdefault)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("adding ALL TABLES requires the publication to have default
publication parameter values"),
+ errdetail("ALL TABLES or ALL SEQUENCES flag should not be set and no
tables/schemas should be associated."),
+ errhint("Use ALTER PUBLICATION ... RESET to reset the publication"));
+
+ AlterPublicationSetAllTables(rel, tup);
+ }
+

Why do we need this self-imposed restriction?

See reply to comment 1.

======
src/include/nodes/parsenodes.h

6.
List *pubobjects; /* Optional list of publication objects */
+ bool for_all_tables; /* Special publication for all tables in db */
AlterPublicationAction action; /* What action to perform with the given
* objects */
} AlterPublicationStmt;

There is no such "FOR" syntax like ALTER PUBLICATION ... FOR ALL
TABLES, so I felt just 'puballtables' might be a better member name.

We have the same variable name in CreatePublicationStmt. I feel
keeping the name as 'for_all_tables' will keep it consistent and
easier to understand.

======
src/test/regress/sql/publication.sql

7.
Don't uppercase any of the publication parameters because they never
appear in the docs/examples like that.

~

8.
So that the last command is the one being tested, I felt that all the
test cases should be doing RESET *first* instead of last.

~~~

9.
You don't always need to use RESET. There should also be some tests
using an "empty" publication just to be sure it works. e.g

CREATE PUBLICATION pub_empty;
ALTER PUBLICATION pub_empty ADD ALL TABLES;

~~~

10.
As commented earlier, I felt the rules were too restrictive. So I
think some test cases can be removed.

~~~

11.
+-- Tests for ALTER PUBLICATION ... ADD ALL TABLES

~

I noticed there is a "--
======================================================" separator
between the major groups of tests.

11a. Should use this separator in patch 0001 for the RESET group of tests

11b. Should use this separator in patch 0002 for the ADD ALL TABLES
groups of tests

~~~

12.
+-- Can't add ALL TABLES to 'ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+

This test case seems to belong earlier, near the 'FOR TABLE' and the
'TABLES IN SCHEMA' tests.

I saw the patch needed a rebase. I have rebased it.
I have also addressed the remaining comments in this email and
comments in the email [2]/messages/by-id/CAHut+Pv4d9EAjDQiOHiu2BrYP3ZA-oJgsgGZdygBaZnWDR7sDA@mail.gmail.com.

While addressing the comments I saw there were a couple of race
conditions when we run 'ALTER PUBLICATION ... RESET and ALTER
PUBLICATION ... ADD TABLE concurrently' and
'ALTER PUBLICATION ... ADD ALL TABLES and ALTER PUBLICATION ... ADD
TABLE concurrently'
I have addressed these in the v29 patch.
Will address comments for 0003 and 0004 patch by Peter and comments by
Shveta in next version.

[1]: https://www.postgresql.org/docs/current/sql-altertable.html
[2]: /messages/by-id/CAHut+Pv4d9EAjDQiOHiu2BrYP3ZA-oJgsgGZdygBaZnWDR7sDA@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v29-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchapplication/octet-stream; name=v29-0001-Add-RESET-clause-to-Alter-Publication-which-will.patchDownload
From 23ef8bf32b7292b5df4cccfdebcf40852fdc188d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 30 Oct 2025 10:52:56 +0530
Subject: [PATCH v29 1/4] Add RESET clause to Alter Publication which will
 reset the publication with default values.

This patch adds a new RESET clause to ALTER PUBLICATION which will reset
the publication to the default state. This includes resetting the publication
parameters, setting ALL TABLES and ALL SEQUENCES flags to false and dropping
the relations and schemas that are associated with the publication.
Usage:
ALTER PUBLICATION pub1 RESET;
---
 doc/src/sgml/ref/alter_publication.sgml   | 170 ++++++++++++++--------
 src/backend/commands/publicationcmds.c    | 130 ++++++++++++++++-
 src/backend/parser/gram.y                 |  13 +-
 src/bin/psql/tab-complete.in.c            |   2 +-
 src/include/catalog/pg_publication.h      |   8 +
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/publication.out |  58 ++++++++
 src/test/regress/sql/publication.sql      |  34 +++++
 8 files changed, 349 insertions(+), 67 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 028770f2149..7d7e6341921 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -27,6 +27,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> DROP <replac
 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> 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>
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>where <replaceable class="parameter">publication_object</replaceable> is one of:</phrase>
 
@@ -49,46 +50,119 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    The command <command>ALTER PUBLICATION</command> can change the attributes
-   of a publication.
-  </para>
+   of a publication. There are several subforms described below.
 
-  <para>
-   The first three variants change which tables/schemas are part of the
-   publication.  The <literal>SET</literal> clause will replace the list of
-   tables/schemas in the publication with the specified list; the existing
-   tables/schemas that were present in the publication will be removed.  The
-   <literal>ADD</literal> and <literal>DROP</literal> clauses will add and
-   remove one or more tables/schemas from the publication.  Note that adding
-   tables/schemas to a publication that is already subscribed to will require an
-   <link linkend="sql-altersubscription-params-refresh-publication">
-   <literal>ALTER SUBSCRIPTION ... REFRESH PUBLICATION</literal></link> action on the
-   subscribing side in order to become effective. Note also that
-   <literal>DROP TABLES IN SCHEMA</literal> will not drop any schema tables
-   that were specified using
-   <link linkend="sql-createpublication-params-for-table"><literal>FOR TABLE</literal></link>/
-   <literal>ADD TABLE</literal>.
-  </para>
+  <variablelist>
+   <varlistentry>
+    <term><literal>ADD <replaceable class="parameter">publication_object</replaceable> [, ...]</literal></term>
+    <listitem>
+     <para>
+      This form adds one or more tables/schemas to the publication.
+      Note that adding tables/schemas to a publication that is already
+      subscribed to will require an
+      <link linkend="sql-altersubscription-params-refresh-publication">
+      <literal>ALTER SUBSCRIPTION ... REFRESH PUBLICATION</literal></link>
+      action on the subscribing side for the changes to take effect.
+     </para>
+    </listitem>
+   </varlistentry>
 
-  <para>
-   The fourth variant of this command listed in the synopsis can change
-   all of the publication properties specified in
-   <xref linkend="sql-createpublication"/>.  Properties not mentioned in the
-   command retain their previous settings.
-  </para>
+   <varlistentry>
+    <term><literal>SET <replaceable class="parameter">publication_object</replaceable> [, ...]</literal></term>
+    <listitem>
+     <para>
+      This form replaces the list of tables/schemas in the publication with the
+      specified list; the existing tables/schemas that were present in the
+      publication are removed.
+     </para>
+    </listitem>
+   </varlistentry>
 
-  <para>
-   The remaining variants change the owner and the name of the publication.
+   <varlistentry>
+    <term><literal>DROP <replaceable class="parameter">publication_object</replaceable> [, ...]</literal></term>
+    <listitem>
+     <para>
+      This form removes one or more tables/schemas from the publication.
+      Note also that <literal>DROP TABLES IN SCHEMA</literal> will not drop any
+      schema tables that were specified using
+      <link linkend="sql-createpublication-params-for-table"><literal>FOR TABLE</literal></link>
+      or <literal>ADD TABLE</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
+    <listitem>
+     <para>
+      This form can change all of the publication properties specified in
+      <xref linkend="sql-createpublication"/>. Properties not mentioned in the
+      command retain their previous settings. It is not applicable to
+      sequences.
+     </para>
+     <caution>
+      <para>
+       Altering the <literal>publish_via_partition_root</literal> parameter can
+       lead to data loss or duplication at the subscriber because it changes
+       the identity and schema of the published tables. Note this happens only
+       when a partition root table is specified as the replication target.
+      </para>
+      <para>
+       This problem can be avoided by refraining from modifying partition leaf
+       tables after the <command>ALTER PUBLICATION ... SET</command> until the
+       <link linkend="sql-altersubscription"><command>ALTER SUBSCRIPTION ... REFRESH PUBLICATION</command></link>
+       is executed and by only refreshing using the <literal>copy_data = off</literal>
+       option.
+      </para>
+     </caution>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }</literal></term>
+    <listitem>
+     <para>
+      This form changes the owner of the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RENAME TO <replaceable>new_name</replaceable></literal></term>
+    <listitem>
+     <para>
+      This form changes the name of the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET</literal></term>
+    <listitem>
+     <para>
+      This form resets the publication to its default state. This includes
+      resetting all publication parameters, setting
+      <link linkend="catalog-pg-publication"><structname>pg_publication</structname></link>.<structfield>puballtables</structfield>
+      and
+      <link linkend="catalog-pg-publication"><structname>pg_publication</structname></link>.<structfield>puballsequences</structfield>
+      to <literal>false</literal>, and removing all tables and schemas that were
+      explicitly added to the publication.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
   </para>
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
    or <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>
@@ -156,32 +230,6 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
-    <listitem>
-     <para>
-      This clause alters publication parameters originally set by
-      <xref linkend="sql-createpublication"/>.  See there for more information.
-      This clause is not applicable to sequences.
-     </para>
-     <caution>
-      <para>
-       Altering the <literal>publish_via_partition_root</literal> parameter can
-       lead to data loss or duplication at the subscriber because it changes
-       the identity and schema of the published tables. Note this happens only
-       when a partition root table is specified as the replication target.
-      </para>
-      <para>
-       This problem can be avoided by refraining from modifying partition leaf
-       tables after the <command>ALTER PUBLICATION ... SET</command> until the
-       <link linkend="sql-altersubscription"><command>ALTER SUBSCRIPTION ... REFRESH PUBLICATION</command></link>
-       is executed and by only refreshing using the <literal>copy_data = off</literal>
-       option.
-      </para>
-     </caution>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><replaceable class="parameter">new_owner</replaceable></term>
     <listitem>
@@ -240,6 +288,12 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Reset the publication <structname>production_publication</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication RESET;
 </programlisting></para>
  </refsect1>
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1faf3a8c372..4f8342f721c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -90,12 +90,12 @@ parse_publication_options(ParseState *pstate,
 	*publish_generated_columns_given = false;
 
 	/* defaults */
-	pubactions->pubinsert = true;
-	pubactions->pubupdate = true;
-	pubactions->pubdelete = true;
-	pubactions->pubtruncate = true;
-	*publish_via_partition_root = false;
-	*publish_generated_columns = PUBLISH_GENCOLS_NONE;
+	pubactions->pubinsert = PUB_DEFAULT_ACTION_INSERT;
+	pubactions->pubupdate = PUB_DEFAULT_ACTION_UPDATE;
+	pubactions->pubdelete = PUB_DEFAULT_ACTION_DELETE;
+	pubactions->pubtruncate = PUB_DEFAULT_ACTION_TRUNCATE;
+	*publish_via_partition_root = PUB_DEFAULT_VIA_ROOT;
+	*publish_generated_columns = PUB_DEFAULT_GENCOLS;
 
 	/* Parse options */
 	foreach(lc, options)
@@ -1209,6 +1209,122 @@ InvalidatePublicationRels(List *relids)
 		CacheInvalidateRelcacheAll();
 }
 
+/*
+ * Reset the publication.
+ *
+ * Reset the publication parameters, setting ALL TABLES and ALL SEQUENCES flag
+ * to false and drop all relations and schemas that are associated with the
+ * publication.
+ */
+static void
+AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
+					  Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	Oid			pubid = pubform->oid;
+	List	   *schemaids = NIL;
+	List	   *rels = NIL;
+	List	   *relids = NIL;
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* RESET publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to RESET publication"));
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Reset the publication parameters */
+	values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_INSERT);
+	replaces[Anum_pg_publication_pubinsert - 1] = true;
+
+	values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_UPDATE);
+	replaces[Anum_pg_publication_pubupdate - 1] = true;
+
+	values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_DELETE);
+	replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+	values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(PUB_DEFAULT_ACTION_TRUNCATE);
+	replaces[Anum_pg_publication_pubtruncate - 1] = true;
+
+	values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(PUB_DEFAULT_VIA_ROOT);
+	replaces[Anum_pg_publication_pubviaroot - 1] = true;
+
+	values[Anum_pg_publication_pubgencols - 1] = CharGetDatum(PUB_DEFAULT_GENCOLS);
+	replaces[Anum_pg_publication_pubgencols - 1] = true;
+
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(false);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	values[Anum_pg_publication_puballsequences - 1] = BoolGetDatum(false);
+	replaces[Anum_pg_publication_puballsequences - 1] = true;
+
+	/*
+	 * Lock the publication so nobody else can do anything with it. This
+	 * prevents concurrent publication parameter changes, add/drop tables(s)
+	 * to the publication and add/drop schema(s) to the publication.
+	 */
+	LockDatabaseObject(PublicationRelationId, pubid, 0,
+					   AccessExclusiveLock);
+
+	/*
+	 * It is possible that by the time we acquire the lock on publication,
+	 * concurrent DDL has removed it. We can test this by checking the
+	 * existence of publication. We get the tuple again to avoid the risk of
+	 * any publication option getting changed.
+	 */
+	tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("publication \"%s\" does not exist",
+					   stmt->pubname));
+
+	if (pubform->puballtables)
+		CacheInvalidateRelcacheAll();
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+	/* Remove the associated schemas from the publication */
+	schemaids = GetPublicationSchemas(pubid);
+
+	/*
+	 * Schema lock is held until the publication is altered to prevent
+	 * concurrent schema deletion.
+	 */
+	LockSchemaList(schemaids);
+
+	/* Remove Schemas */
+	PublicationDropSchemas(pubid, schemaids, true);
+
+	/* Get all relations associated with the publication */
+	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+
+	foreach_oid(relid, relids)
+	{
+		PublicationRelInfo *rel;
+
+		rel = palloc(sizeof(PublicationRelInfo));
+		rel->whereClause = NULL;
+		rel->columns = NIL;
+		rel->relation = table_open(relid, ShareUpdateExclusiveLock);
+		rels = lappend(rels, rel);
+	}
+
+	/* Remove the associated relations from the publication */
+	PublicationDropTables(pubid, rels, true);
+	CloseTableList(rels);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1553,6 +1669,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	else if (stmt->action == AP_Reset)
+		AlterPublicationReset(pstate, stmt, rel, tup);
 	else
 	{
 		List	   *relations = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3a0a354a9c..a8b9ae6182d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10904,15 +10904,17 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *
  * ALTER PUBLICATION name ADD pub_obj [, ...]
  *
- * ALTER PUBLICATION name DROP pub_obj [, ...]
- *
  * ALTER PUBLICATION name SET pub_obj [, ...]
  *
+ * ALTER PUBLICATION name DROP pub_obj [, ...]
+ *
  * pub_obj is one of:
  *
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name RESET
+ *
  *****************************************************************************/
 
 AlterPublicationStmt:
@@ -10954,6 +10956,13 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name RESET
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->action = AP_Reset;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 20d7a65c614..83599de2225 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2289,7 +2289,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER PUBLICATION <name> */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny))
-		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "SET");
+		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..641017e9496 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -152,6 +152,14 @@ extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
 extern List *GetRelationPublications(Oid relid);
 
+/* default values for flags and publication parameters */
+#define PUB_DEFAULT_ACTION_INSERT true
+#define PUB_DEFAULT_ACTION_UPDATE true
+#define PUB_DEFAULT_ACTION_DELETE true
+#define PUB_DEFAULT_ACTION_TRUNCATE true
+#define PUB_DEFAULT_VIA_ROOT false
+#define PUB_DEFAULT_GENCOLS PUBLISH_GENCOLS_NONE
+
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
  * which allows callers to specify which partitions of partitioned tables
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..8cf75724a7b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4326,6 +4326,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_Reset,					/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..ce5b3b649d5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2009,7 +2009,65 @@ Tables:
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- ======================================================
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
 RESET client_min_messages;
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ERROR:  must be superuser to RESET publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+-- Verify that 'ALL TABLES', 'ALL SEQUENCES' flags are reset
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t             | t       | t       | t       | t         | none              | f
+(1 row)
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Verify that a publication RESET removes the associated tables and
+-- schemas, and sets default values for publication parameters 'publish',
+-- 'publish_via_partition_root', and 'publish_generated_columns'.
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset SET (publish_via_partition_root = 'true');
+ALTER PUBLICATION testpub_reset SET (publish = '');
+ALTER PUBLICATION testpub_reset SET (publish_generated_columns = stored);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | f       | f       | f       | f         | stored            | t
+Tables:
+    "pub_sch1.tbl1"
+Tables from schemas:
+    "public"
+
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+-- ======================================================
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
 CREATE TABLE testpub_insert_onconfl_no_ri (a int unique, b int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..f0432f67b4a 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1267,9 +1267,43 @@ ALTER PUBLICATION pub2 SET TABLE gencols(a, gen1);
 DROP PUBLICATION pub1;
 DROP PUBLICATION pub2;
 DROP TABLE gencols;
+-- ======================================================
 
+-- Tests for ALTER PUBLICATION ... RESET
+CREATE SCHEMA pub_sch1;
+CREATE TABLE pub_sch1.tbl1 (a int);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
 RESET client_min_messages;
 
+-- Verify that only superuser can reset a publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset RESET; -- fail - must be superuser
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+
+-- Verify that 'ALL TABLES', 'ALL SEQUENCES' flags are reset
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+-- Verify that a publication RESET removes the associated tables and
+-- schemas, and sets default values for publication parameters 'publish',
+-- 'publish_via_partition_root', and 'publish_generated_columns'.
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1, TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset SET (publish_via_partition_root = 'true');
+ALTER PUBLICATION testpub_reset SET (publish = '');
+ALTER PUBLICATION testpub_reset SET (publish_generated_columns = stored);
+\dRp+ testpub_reset
+ALTER PUBLICATION testpub_reset RESET;
+\dRp+ testpub_reset
+
+DROP PUBLICATION testpub_reset;
+DROP TABLE pub_sch1.tbl1;
+DROP SCHEMA pub_sch1;
+-- ======================================================
+
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
 -- when the target table is published.
 CREATE TABLE testpub_insert_onconfl_no_ri (a int unique, b int);
-- 
2.34.1

v29-0003-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v29-0003-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 98979e7701f1d5133df90a08e8ed5a20d5e30398 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 4 Dec 2025 16:51:47 +0530
Subject: [PATCH v29 3/4] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);
or
ALTER PUBLICATION pub1 ADD ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/ref/alter_publication.sgml       |  26 ++-
 doc/src/sgml/ref/create_publication.sgml      |  47 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          |  99 +++++++---
 src/backend/commands/publicationcmds.c        | 140 ++++++++-----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  36 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  25 +--
 src/backend/utils/cache/relcache.c            |  17 +-
 src/bin/pg_dump/pg_dump.c                     |  56 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl              |  20 ++
 src/bin/psql/describe.c                       |  58 +++++-
 src/bin/psql/tab-complete.in.c                |  10 +
 src/include/catalog/pg_publication.h          |  10 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   5 +-
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  59 +++++-
 src/test/regress/sql/publication.sql          |  24 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 186 ++++++++++++++++++
 24 files changed, 725 insertions(+), 135 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..a4d32de58ec 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 0ab2a9d007e..1ceaeaec772 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -24,7 +24,7 @@ PostgreSQL documentation
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
 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_drop_object</replaceable> [, ...]
-ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_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> 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>
@@ -43,6 +43,10 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -93,10 +97,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
    </varlistentry>
 
    <varlistentry>
-    <term><literal>ADD ALL TABLES</literal></term>
+    <term><literal>ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]</literal></term>
     <listitem>
      <para>
-      This form adds all tables to the publication. This requires the
+      This form adds all tables, except those listed in the
+      <literal>EXCEPT</literal> clause, to the publication. This requires the
       publication to not have any existing table or schema list.
      </para>
     </listitem>
@@ -167,8 +172,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
-   Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD ALL TABLES</literal>,
+   Adding or excluding a table to a publication additionally requires owning
+   that table. The <literal>ADD ALL TABLES</literal>,
    <literal>ADD TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
@@ -210,7 +215,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
       table name, only that table is affected.  If <literal>ONLY</literal> is not
       specified, the table and all its descendant tables (if any) are
       affected.  Optionally, <literal>*</literal> can be specified after the table
-      name to explicitly indicate that descendant tables are included.
+      name to explicitly indicate that descendant tables are affected. For
+      partitioned tables, <literal>ONLY</literal> does not have any effect.
      </para>
 
      <para>
@@ -293,6 +299,14 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA marketing, sales;
 </programlisting>
   </para>
 
+  <para>
+   Alter publication <structname>production_publication</structname> to publish
+   all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departments);
+</programlisting></para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..1280837f995 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +190,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>
+     <para>
+      The partitioned table or its partitions are excluded from the publication
+      based on the parameter <literal>publish_via_partition_root</literal>.
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -467,6 +502,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 0994220c53d..39c2cc2bf43 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,44 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = GetRelationPublications(ancestor, false);
+		List	   *aschemapubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+			{
+				aexceptpubids = GetRelationPublications(ancestor, true);
+				set_top = !list_member_oid(aexceptpubids, puboid);
+			}
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +479,17 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check for partitions of partitioned table which are specified with
+	 * EXCEPT clause and partitioned table is published with
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +506,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,9 +775,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
 List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
 {
 	List	   *result = NIL;
 	CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+			result = lappend_oid(result, pubid);
 	}
 
 	ReleaseSysCacheList(pubrellist);
@@ -774,13 +801,14 @@ GetRelationPublications(Oid relid)
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Gets list of relation oids for a publication that matches the except_flag.
  *
  * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
  * should use GetAllPublicationRelations().
  */
 List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+						bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +833,11 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
+
 	}
 
 	systable_endscan(scan);
@@ -866,13 +897,19 @@ GetAllTablesPublications(void)
  * publication.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT,
+										 true);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +928,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +950,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1207,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1178,7 +1218,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				relids = GetPublicationRelations(pub_elem->oid,
 												 pub_elem->pubviaroot ?
 												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+												 PUBLICATION_PART_LEAF,
+												 false);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1408,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 7a9020ad43f..9a17dfc9d35 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,39 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +227,11 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +306,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +333,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -355,7 +394,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +418,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -515,7 +555,7 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * its leaves.
 		 */
 		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+										 PUBLICATION_PART_ALL, false);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -923,56 +963,54 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
-	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &schemaidlist);
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
+	if (relations != NIL)
+	{
+		List	   *rels;
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1041,7 +1079,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 						   AccessShareLock);
 
 		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+											  PUBLICATION_PART_ROOT, false);
 
 		foreach(lc, root_relids)
 		{
@@ -1161,7 +1199,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 */
 		if (root_relids == NIL)
 			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+											 PUBLICATION_PART_ALL, false);
 		else
 		{
 			/*
@@ -1307,7 +1345,10 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	PublicationDropSchemas(pubid, schemaids, true);
 
 	/* Get all relations associated with the publication */
-	relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT);
+	if (pubform->puballtables)
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, true);
+	else
+		relids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT, false);
 
 	foreach_oid(relid, relids)
 	{
@@ -1351,7 +1392,7 @@ CheckAlterPublicationAllTables(HeapTuple tup)
 					   NameStr(pubform->pubname)),
 				errdetail("ALL TABLES cannot be added when schemas are associated with the publication."));
 
-	pubobjs = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+	pubobjs = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT, false);
 	if (list_length(pubobjs))
 		ereport(ERROR,
 				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
@@ -1436,7 +1477,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+														PUBLICATION_PART_ROOT,
+														false);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1537,6 +1579,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1587,7 +1630,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT,
+										  false);
 
 		foreach(lc, reloids)
 		{
@@ -1954,6 +1998,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -2026,6 +2071,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 07e5b95782e..14fd782d05a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), false) != NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9d648ccb47b..2ae51e5bfe1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				except_pub_obj_list opt_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> ExceptPublicationObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10761,6 +10763,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10801,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10877,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = (List *) $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10897,6 +10904,28 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_except_clause:
+			EXCEPT opt_table '(' except_pub_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
+ExceptPublicationObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+except_pub_obj_list: ExceptPublicationObjSpec
+					{ $$ = list_make1($1); }
+			| except_pub_obj_list ',' ExceptPublicationObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
@@ -10913,7 +10942,7 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
- * ALTER PUBLICATION name ADD ALL TABLES
+ * ALTER PUBLICATION name ADD ALL TABLES [EXCEPT [TABLE] (table_name [, ...])]
  *
  * ALTER PUBLICATION name RESET
  *
@@ -10958,10 +10987,11 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
-			| ALTER PUBLICATION name ADD_P ALL TABLES
+			| ALTER PUBLICATION name ADD_P ALL TABLES opt_except_clause
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
 					n->pubname = $3;
+					n->pubobjects = (List *) $7;
 					n->for_all_tables = true;
 					n->action = AP_AddObjects;
 					$$ = (Node *)n;
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 942e1abdb58..a9593c5d9da 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = GetRelationPublications(relid, false);
+		List	   *exceptTablePubids = GetRelationPublications(relid, true);
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2195,22 +2196,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2214,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2230,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2310,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc9084..96dd0ccf41a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5794,6 +5794,8 @@ void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
 	List	   *puboids;
+	List	   *alltablespuboids;
+	List	   *exceptpuboids = NIL;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,9 +5833,10 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	puboids = GetRelationPublications(relid, false);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+	exceptpuboids = GetRelationPublications(relid, true);
 
 	if (relation->rd_rel->relispartition)
 	{
@@ -5845,14 +5848,19 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			Oid			ancestor = lfirst_oid(lc);
 
 			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+											 GetRelationPublications(ancestor, false));
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   GetRelationPublications(ancestor, true));
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5891,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5901,6 +5909,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 			pub_contains_invalid_column(pubid, relation, ancestors,
 										pubform->pubviaroot,
 										pubform->pubgencols,
+										pubform->puballtables,
 										&invalid_column_list,
 										&invalid_gen_col))
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2445085dbbd..f8250b000d8 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4662,7 +4664,34 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		SimplePtrListCell *cell;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has except tables */
+		for (cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4831,6 +4860,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4842,8 +4872,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4854,6 +4892,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4869,6 +4908,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4880,6 +4920,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		char	   *prexcept = pg_strdup(PQgetvalue(res, i, i_prexcept));
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4893,7 +4934,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (strcmp(prexcept, "f") == 0)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4934,6 +4979,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (strcmp(prexcept, "t") == 0)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11812,6 +11860,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20182,6 +20233,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..f3c30f3be37 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -443,6 +445,17 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+	{
+		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+		/* Sort by publication name, since (namespace, name) match the rel */
+		cmpval = strcmp(probj1->publication->dobj.name,
+						probj2->publication->dobj.name);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA)
 	{
 		PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1;
@@ -1715,6 +1728,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..381b7c39bb0 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b1175e0c08b..63036ec7656 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2293,11 +2293,17 @@ match_previous_words(int pattern_id,
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
 			 ends_with(prev_wd, ','))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE") && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 	/*
 	 * "ALTER PUBLICATION <name> SET TABLE <name> WHERE (" - complete with
@@ -3623,6 +3629,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
+		COMPLETE_WITH("EXCEPT TABLE", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH("WITH (");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 641017e9496..c7a61f3194c 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern List *GetRelationPublications(Oid relid, bool except_flag);
 
 /* default values for flags and publication parameters */
 #define PUB_DEFAULT_ACTION_INSERT true
@@ -176,9 +177,10 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
+									 bool except_flag);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -189,7 +191,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..0ad5d28754d 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
 										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										char pubgencols_type, bool puballtables,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index c22d75e80a2..a14ecedb27f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index ec12f7cfbaa..06b54d8c834 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,37 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +262,25 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
@@ -2013,6 +2054,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
 RESET client_min_messages;
@@ -2126,8 +2168,21 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES;
  regress_publication_user | t          | f             | f       | f       | f       | f         | stored            | t
 (1 row)
 
+-- Verify adding EXCEPT TABLE
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "pub_sch1.tbl1"
+    "pub_sch1.tbl2"
+
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 -- ======================================================
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 5259331137b..292deb52b93 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,33 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
@@ -1272,6 +1285,7 @@ DROP TABLE gencols;
 -- Tests for ALTER PUBLICATION ... RESET
 CREATE SCHEMA pub_sch1;
 CREATE TABLE pub_sch1.tbl1 (a int);
+CREATE TABLE pub_sch1.tbl2 (a int);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
 RESET client_min_messages;
@@ -1341,8 +1355,14 @@ ALTER PUBLICATION testpub_reset SET (publish = '', publish_via_partition_root =
 ALTER PUBLICATION testpub_reset ADD ALL TABLES;
 \dRp+ testpub_reset
 
+-- Verify adding EXCEPT TABLE
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset
+
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
 DROP SCHEMA pub_sch1;
 -- ======================================================
 
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..096e0606365
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,186 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+	CREATE TABLE public.tab1(a int);
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+	CREATE TABLE public.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Insert some data and verify that inserted data is not replicated
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_schema RESET;
+	ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE (sch1.tab1, public.tab1);
+	INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM public.tab1");
+is($result, qq(0||), 'check rows on subscriber catchup');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+
+# Check behaviour of publish_via_partition_root and EXCEPT clause with
+# partitioned table or partiitions of partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	INSERT INTO sch1.t1 VALUES (1);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+));
+
+# publish_via_partition_root = false and EXCEPT sch1.part1
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = false and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.t1);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2
+3), 'check rows on partitions');
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# publish_via_partition_root = true and EXCEPT sch1.t1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+# publish_via_partition_root = true and EXCEPT sch1.part1
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_part RESET;
+	ALTER PUBLICATION tap_pub_part ADD ALL TABLES EXCEPT (sch1.part1);
+	ALTER PUBLICATION tap_pub_part SET (publish_via_partition_root);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_part REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres', "INSERT INTO sch1.t1 VALUES (3)");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is( $result, qq(1
+2
+3), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on partitions');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

v29-0002-Support-ADD-ALL-TABLES-in-ALTER-PUBLICATION.patchapplication/octet-stream; name=v29-0002-Support-ADD-ALL-TABLES-in-ALTER-PUBLICATION.patchDownload
From acb7ddbb4a53776c9451a210bed3d53781c81e22 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 1 Dec 2025 16:17:53 +0530
Subject: [PATCH v29 2/4] Support ADD ALL TABLES in ALTER PUBLICATION

This patch adds support for using ADD ALL TABLES in ALTER PUBLICATION,
allowing an existing publication to be changed into an ALL TABLES
publication. This command is permitted only when the publication have
no tables or schemas explicitly added and its ALL TABLES flag is not
set.
Usage:
ALTER PUBLICATION pub1 ADD ALL TABLES
---
 doc/src/sgml/logical-replication.sgml     | 10 +--
 doc/src/sgml/ref/alter_publication.sgml   | 14 +++-
 src/backend/commands/publicationcmds.c    | 83 +++++++++++++++++++++--
 src/backend/parser/gram.y                 | 10 +++
 src/bin/psql/tab-complete.in.c            |  2 +-
 src/include/nodes/parsenodes.h            |  1 +
 src/test/regress/expected/publication.out | 62 +++++++++++++++++
 src/test/regress/sql/publication.sql      | 42 ++++++++++++
 8 files changed, 214 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index aa013f348d4..c420469feaa 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2550,10 +2550,12 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 7d7e6341921..0ab2a9d007e 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -24,6 +24,7 @@ PostgreSQL documentation
 ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD <replaceable class="parameter">publication_object</replaceable> [, ...]
 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_drop_object</replaceable> [, ...]
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable> ADD ALL TABLES
 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> 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>
@@ -91,6 +92,16 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ADD ALL TABLES</literal></term>
+    <listitem>
+     <para>
+      This form adds all tables to the publication. This requires the
+      publication to not have any existing table or schema list.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>SET ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -157,7 +168,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal>,
+   The <literal>ADD ALL TABLES</literal>,
+   <literal>ADD TABLES IN SCHEMA</literal>,
    <literal>SET TABLES IN SCHEMA</literal> to a publication and
    <literal>RESET</literal> of publication requires the invoking user to be a
    superuser. To alter the owner, you must be able to
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 4f8342f721c..7a9020ad43f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1325,6 +1325,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt,
 	CloseTableList(rels);
 }
 
+/*
+ * Check whether we can alter the publication to add ALL TABLES.
+ *
+ * It is not allowed if the publication already is defined as ALL TABLES, or
+ * if there are any schemas or tables associated with the publication.
+ */
+static void
+CheckAlterPublicationAllTables(HeapTuple tup)
+{
+	List	   *pubobjs;
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+	if (pubform->puballtables)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("publication \"%s\" is already defined as FOR ALL TABLES",
+					   NameStr(pubform->pubname)));
+
+	pubobjs = GetPublicationSchemas(pubform->oid);
+	if (list_length(pubobjs))
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("publication \"%s\" has schemas associated with it",
+					   NameStr(pubform->pubname)),
+				errdetail("ALL TABLES cannot be added when schemas are associated with the publication."));
+
+	pubobjs = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+	if (list_length(pubobjs))
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("publication \"%s\" has tables associated with it",
+					   NameStr(pubform->pubname)),
+				errdetail("ALL TABLES cannot be added when tables are associated with the publication."));
+}
+
+/*
+ * Set publication to publish all tables.
+ */
+static void
+AlterPublicationSetAllTables(Relation rel, HeapTuple tup)
+{
+	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	bool		nulls[Natts_pg_publication];
+	bool		replaces[Natts_pg_publication];
+	Datum		values[Natts_pg_publication];
+
+	/* Add ALL TABLES to the publication requires superuser */
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to ADD ALL TABLES to the publication"));
+
+	/* Lock the publication so nobody else can do anything with it. */
+	LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
+					   AccessExclusiveLock);
+
+	CheckAlterPublicationAllTables(tup);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+	memset(replaces, false, sizeof(replaces));
+
+	/* Set ALL TABLES flag */
+	values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true);
+	replaces[Anum_pg_publication_puballtables - 1] = true;
+
+	tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+							replaces);
+
+	/* Update the catalog. */
+	CatalogTupleUpdate(rel, &tup->t_self, tup);
+}
+
 /*
  * Add or remove table to/from publication.
  */
@@ -1667,6 +1740,9 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
+	if (stmt->for_all_tables)
+		AlterPublicationSetAllTables(rel, tup);
+
 	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
 	else if (stmt->action == AP_Reset)
@@ -1680,10 +1756,7 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &schemaidlist);
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
-
 		heap_freetuple(tup);
-
 		/* Lock the publication so nobody else can do anything with it. */
 		LockDatabaseObject(PublicationRelationId, pubid, 0,
 						   AccessExclusiveLock);
@@ -1692,7 +1765,7 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		 * It is possible that by the time we acquire the lock on publication,
 		 * concurrent DDL has removed it. We can test this by checking the
 		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
+		 * of any publication option or ALL TABLES flag getting changed.
 		 */
 		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
 		if (!HeapTupleIsValid(tup))
@@ -1701,6 +1774,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 					errmsg("publication \"%s\" does not exist",
 						   stmt->pubname));
 
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
 							   schemaidlist != NIL);
 		AlterPublicationSchemas(stmt, tup, schemaidlist);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a8b9ae6182d..9d648ccb47b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -10913,6 +10913,8 @@ pub_all_obj_type_list:	PublicationAllObjSpec
  *		TABLE table_name [, ...]
  *		TABLES IN SCHEMA schema_name [, ...]
  *
+ * ALTER PUBLICATION name ADD ALL TABLES
+ *
  * ALTER PUBLICATION name RESET
  *
  *****************************************************************************/
@@ -10956,6 +10958,14 @@ AlterPublicationStmt:
 					n->action = AP_DropObjects;
 					$$ = (Node *) n;
 				}
+			| ALTER PUBLICATION name ADD_P ALL TABLES
+				{
+					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
+					n->pubname = $3;
+					n->for_all_tables = true;
+					n->action = AP_AddObjects;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RESET
 				{
 					AlterPublicationStmt *n = makeNode(AlterPublicationStmt);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 83599de2225..b1175e0c08b 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2292,7 +2292,7 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET");
 	/* ALTER PUBLICATION <name> ADD */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
-		COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
+		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8cf75724a7b..c22d75e80a2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4342,6 +4342,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index ce5b3b649d5..ec12f7cfbaa 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2064,6 +2064,68 @@ ALTER PUBLICATION testpub_reset RESET;
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 (1 row)
 
+-- ======================================================
+-- Tests for ALTER PUBLICATION ... ADD ALL TABLES
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; -- fail - must be superuser
+ERROR:  must be superuser to ADD ALL TABLES to the publication
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+-- Can add ALL TABLES to an empty publication
+DROP PUBLICATION testpub_reset;
+CREATE PUBLICATION testpub_reset;
+WARNING:  "wal_level" is insufficient to publish logical changes
+HINT:  Set "wal_level" to "logical" before creating subscriptions.
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Can't add ALL TABLES to 'ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  publication "testpub_reset" is already defined as FOR ALL TABLES
+-- Can add ALL TABLES to ALL SEQUENCES publication
+DROP PUBLICATION testpub_reset;
+CREATE PUBLICATION testpub_reset for ALL SEQUENCES;
+WARNING:  "wal_level" is insufficient to publish logical changes
+HINT:  Set "wal_level" to "logical" before creating subscriptions.
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | t             | t       | t       | t       | t         | none              | f
+(1 row)
+
+-- Can't add ALL TABLES to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  publication "testpub_reset" has tables associated with it
+DETAIL:  ALL TABLES cannot be added when tables are associated with the publication.
+-- Can't add ALL TABLES to 'TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ERROR:  publication "testpub_reset" has schemas associated with it
+DETAIL:  ALL TABLES cannot be added when schemas are associated with the publication.
+-- Can add ALL TABLES when the 'publish', 'publish_via_partition_root',
+-- 'publish_generated_columns' parameters does not have default value
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset SET (publish = '', publish_via_partition_root = 'true', publish_generated_columns = stored);
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+                                                   Publication testpub_reset
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | f       | f       | f       | f         | stored            | t
+(1 row)
+
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP SCHEMA pub_sch1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index f0432f67b4a..5259331137b 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1299,6 +1299,48 @@ ALTER PUBLICATION testpub_reset SET (publish_generated_columns = stored);
 ALTER PUBLICATION testpub_reset RESET;
 \dRp+ testpub_reset
 
+-- ======================================================
+
+-- Tests for ALTER PUBLICATION ... ADD ALL TABLES
+-- Verify that only superuser can ADD ALL TABLES
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2;
+SET ROLE regress_publication_user2;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES; -- fail - must be superuser
+ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user;
+SET ROLE regress_publication_user;
+
+-- Can add ALL TABLES to an empty publication
+DROP PUBLICATION testpub_reset;
+CREATE PUBLICATION testpub_reset;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+
+-- Can't add ALL TABLES to 'ALL TABLES' publication
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+
+-- Can add ALL TABLES to ALL SEQUENCES publication
+DROP PUBLICATION testpub_reset;
+CREATE PUBLICATION testpub_reset for ALL SEQUENCES;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+
+-- Can't add ALL TABLES to 'FOR TABLE' publication
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+
+-- Can't add ALL TABLES to 'TABLES IN SCHEMA' publication
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public;
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+
+-- Can add ALL TABLES when the 'publish', 'publish_via_partition_root',
+-- 'publish_generated_columns' parameters does not have default value
+ALTER PUBLICATION testpub_reset RESET;
+ALTER PUBLICATION testpub_reset SET (publish = '', publish_via_partition_root = 'true', publish_generated_columns = stored);
+ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+\dRp+ testpub_reset
+
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP SCHEMA pub_sch1;
-- 
2.34.1

v29-0004-Skip-publishing-the-columns-specified-in-FOR-TAB.patchapplication/octet-stream; name=v29-0004-Skip-publishing-the-columns-specified-in-FOR-TAB.patchDownload
From fc82f0f3dc7428959dcd185ccdb6fd436e32355d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 1 Dec 2025 17:16:30 +0530
Subject: [PATCH v29 4/4] Skip publishing the columns specified in FOR TABLE
 EXCEPT

A new "TABLE table_name EXCEPT (column_list)" clause for CREATE/ALTER
PUBLICATION allows one or more columns to be excluded. The publisher
will not send the data of excluded columns to the subscriber.

The new syntax allows specifying excluded column list when creating or
altering a publication. For example:
CREATE PUBLICATION pubname FOR TABLE tabname EXCEPT (col1, col2, col3)
or
ALTER PUBLICATION pubname ADD TABLE tabname EXCEPT (col1, col2, col3)

When column "prexcept" of system catalog "pg_publication_rel" is set
to "true", and column "prattrs" of system catalog "pg_publication_rel"
is not NULL, that means the publication was created with "EXCEPT
(column-list)", and the columns in "prattrs" will be excluded from
being published.

pg_dump is updated to identify and dump the excluded column list of the
publication.

The psql \d family of commands can now display excluded column list. e.g.
psql \dRp+ variant will now display associated "EXCEPT (column_list)" if
any.
---
 doc/src/sgml/catalogs.sgml                    |   5 +-
 doc/src/sgml/logical-replication.sgml         | 106 +++++--
 doc/src/sgml/ref/alter_publication.sgml       |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 +++-
 src/backend/catalog/pg_publication.c          |  75 ++++-
 src/backend/commands/publicationcmds.c        |  52 ++--
 src/backend/parser/gram.y                     |  44 ++-
 src/backend/replication/logical/tablesync.c   |  41 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  70 ++++-
 src/bin/pg_dump/pg_dump.c                     |  45 +--
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/describe.c                       | 262 +++++++++++-------
 src/bin/psql/tab-complete.in.c                |  13 +-
 src/include/catalog/pg_publication.h          |   6 +-
 src/include/catalog/pg_publication_rel.h      |   5 +-
 src/test/regress/expected/publication.out     |  88 ++++++
 src/test/regress/sql/publication.sql          |  54 ++++
 src/test/subscription/meson.build             |   1 +
 .../t/038_rep_changes_except_collist.pl       | 193 +++++++++++++
 19 files changed, 906 insertions(+), 217 deletions(-)
 create mode 100644 src/test/subscription/t/038_rep_changes_except_collist.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a4d32de58ec..70144b67213 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6586,7 +6586,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <structfield>prexcept</structfield> <type>bool</type>
       </para>
       <para>
-       True if the relation must be excluded
+       True if the column list or relation must be excluded from publication.
+       If a column list is specified in <literal>prattrs</literal>, then
+       exclude only those columns. If <literal>prattrs</literal> is null,
+       then exclude the entire relation.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c420469feaa..1496e1c28ad 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1379,10 +1379,10 @@ Publications:
   <title>Column Lists</title>
 
   <para>
-   Each publication can optionally specify which columns of each table are
-   replicated to subscribers. The table on the subscriber side must have at
-   least all the columns that are published. If no column list is specified,
-   then all columns on the publisher are replicated.
+   Each publication can optionally specify which columns of each table should be
+   replicated or excluded from replication. The table on the subscriber side
+   must have at least all the columns that are published. If no column list is
+   specified, then all columns on the publisher are replicated.
    See <xref linkend="sql-createpublication"/> for details on the syntax.
   </para>
 
@@ -1396,8 +1396,11 @@ Publications:
 
   <para>
    If no column list is specified, any columns added to the table later are
-   automatically replicated. This means that having a column list which names
-   all columns is not the same as having no column list at all.
+   automatically replicated. However, a normal column list (without
+   <literal>EXCEPT</literal>) only replicates the specified columns and no more.
+   Therefore, having a column list that names all columns is not the same as
+   having no column list at all, as more columns may be added to the table
+   later.
   </para>
 
   <para>
@@ -1409,6 +1412,14 @@ Publications:
    Generated columns can also be specified in a column list. This allows
    generated columns to be published, regardless of the publication parameter
    <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link>. Specifying generated
+   columns using the <literal>EXCEPT</literal> clause excludes those columns
+   from being published, regardless of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> setting. However, for
+   generated columns that are not listed in the <literal>EXCEPT</literal>
+   clause, whether they are published or not still depends on the value of
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
    <literal>publish_generated_columns</literal></link>. See
    <xref linkend="logical-replication-gencols"/> for details.
   </para>
@@ -1430,11 +1441,14 @@ Publications:
 
   <para>
    If a publication publishes <command>UPDATE</command> or
-   <command>DELETE</command> operations, any column list must include the
-   table's replica identity columns (see
-   <xref linkend="sql-altertable-replica-identity"/>).
+   <command>DELETE</command> operations, any column list must include table's
+   replica identity columns and any column list specified with
+   <literal>EXCEPT</literal> clause must not include the table's replica
+   identity columns (see <xref linkend="sql-altertable-replica-identity"/>).
    If a publication publishes only <command>INSERT</command> operations, then
-   the column list may omit replica identity columns.
+   the column list may omit replica identity columns and the column list
+   specified with <literal>EXCEPT</literal> clause may include replica identity
+   columns.
   </para>
 
   <para>
@@ -1479,18 +1493,21 @@ Publications:
    <title>Examples</title>
 
    <para>
-    Create a table <structname>t1</structname> to be used in the following example.
+    Create tables <structname>t1</structname> and <structname>t2</structname> to
+    be used in the following example.
 <programlisting>
 /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
+/* pub # */ CREATE TABLE t2(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
 </programlisting></para>
 
    <para>
     Create a publication <literal>p1</literal>. A column list is defined for
-    table <structname>t1</structname> to reduce the number of columns that will be
-    replicated. Notice that the order of column names in the column list does
-    not matter.
+    table <structname>t1</structname>, and another column list is defined for
+    table <structname>t2</structname> using the <literal>EXCEPT</literal> clause
+    to reduce the number of columns that will be replicated. Note that the order
+    of column names in the column lists does not matter.
 <programlisting>
-/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
+/* pub # */ CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d), t2 EXCEPT (d, a);
 </programlisting></para>
 
     <para>
@@ -1504,6 +1521,7 @@ Publications:
  postgres | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
     "public.t1" (id, a, b, d)
+    "public.t2" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
@@ -1524,23 +1542,41 @@ Indexes:
     "t1_pkey" PRIMARY KEY, btree (id)
 Publications:
     "p1" (id, a, b, d)
+
+/* pub # */ \d t2
+                 Table "public.t2"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id     | integer |           | not null |
+ a      | text    |           |          |
+ b      | text    |           |          |
+ c      | text    |           |          |
+ d      | text    |           |          |
+ e      | text    |           |          |
+Indexes:
+    "t2_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "p1" EXCEPT (a, d)
 </programlisting></para>
 
     <para>
-     On the subscriber node, create a table <structname>t1</structname> which now
-     only needs a subset of the columns that were on the publisher table
-     <structname>t1</structname>, and also create the subscription
+     On the subscriber node, create tables <structname>t1</structname> and
+     <structname>t2</structname> which now only needs a subset of the columns
+     that were on the publisher tables <structname>t1</structname> and
+     <structname>t2</structname>, and also create the subscription
      <literal>s1</literal> that subscribes to the publication
      <literal>p1</literal>.
 <programlisting>
 /* sub # */ CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
+/* sub # */ CREATE TABLE t2(id int, b text, c text, e text, PRIMARY KEY(id));
 /* sub # */ CREATE SUBSCRIPTION s1
 /* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=s1'
 /* sub - */ PUBLICATION p1;
 </programlisting></para>
 
     <para>
-     On the publisher node, insert some rows to table <structname>t1</structname>.
+     On the publisher node, insert some rows to tables <structname>t1</structname>
+     and <structname>t2</structname>.
 <programlisting>
 /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
 /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
@@ -1552,11 +1588,21 @@ Publications:
   2 | a-2 | b-2 | c-2 | d-2 | e-2
   3 | a-3 | b-3 | c-3 | d-3 | e-3
 (3 rows)
+/* pub # */ INSERT INTO t2 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
+/* pub # */ INSERT INTO t2 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
+/* pub # */ INSERT INTO t2 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
+/* pub # */ SELECT * FROM t2 ORDER BY id;
+ id |  a  |  b  |  c  |  d  |  e
+----+-----+-----+-----+-----+-----
+  1 | a-1 | b-1 | c-1 | d-1 | e-1
+  2 | a-2 | b-2 | c-2 | d-2 | e-2
+  3 | a-3 | b-3 | c-3 | d-3 | e-3
+(3 rows)
 </programlisting></para>
 
     <para>
-     Only data from the column list of publication <literal>p1</literal> is
-     replicated.
+     Only data specified by the column lists of publication
+     <literal>p1</literal> is replicated.
 <programlisting>
 /* sub # */ SELECT * FROM t1 ORDER BY id;
  id |  b  |  a  |  d
@@ -1565,6 +1611,13 @@ Publications:
   2 | b-2 | a-2 | d-2
   3 | b-3 | a-3 | d-3
 (3 rows)
+/* sub # */ SELECT * FROM t2 ORDER BY id;
+ id |  b  |  c  |  e
+----+-----+-----+-----
+  1 | b-1 | c-1 | e-1
+  2 | b-2 | c-2 | e-2
+  3 | b-3 | c-3 | e-3
+(3 rows)
 </programlisting></para>
 
   </sect2>
@@ -1661,6 +1714,17 @@ Publications:
    </itemizedlist>
   </para>
 
+  <para>
+   Generated columns specified in the column list with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the value
+   of the <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter. However,
+   generated columns that are not part of the column list with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <link linkend="sql-createpublication-params-with-publish-generated-columns">
+   <literal>publish_generated_columns</literal></link> parameter.
+  </para>
+
   <para>
    The following table summarizes behavior when there are generated columns
    involved in the logical replication. Results are shown for when
diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 1ceaeaec772..701cfa81942 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -42,7 +42,7 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RESET
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
-    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
 
 <phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
 
@@ -314,6 +314,14 @@ ALTER PUBLICATION production_publication ADD ALL TABLES EXCEPT (users, departmen
    <structname>production_publication</structname>:
 <programlisting>
 ALTER PUBLICATION production_publication ADD TABLE users, departments, TABLES IN SCHEMA production;
+</programlisting></para>
+
+  <para>
+   Alter publication <structname>mypublication</structname> to add table
+   <structname>users</structname> except column
+   <structname>security_pin</structname>:
+<programlisting>
+ALTER PUBLICATION production_publication ADD TABLE users EXCEPT (security_pin);
 </programlisting></para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 1280837f995..216e30f08d2 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -37,7 +37,7 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
-    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ EXCEPT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
 
 <phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
 
@@ -100,17 +100,24 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
      </para>
 
      <para>
-      When a column list is specified, only the named columns are replicated.
-      The column list can contain stored generated columns as well. If the
-      column list is omitted, the publication will replicate all non-generated
-      columns (including any added in the future) by default. Stored generated
-      columns can also be replicated if <literal>publish_generated_columns</literal>
-      is set to <literal>stored</literal>. Specifying a column list has no
-      effect on <literal>TRUNCATE</literal> commands. See
+      When a column list without <literal>EXCEPT</literal> is specified, only
+      the named columns are replicated. The column list can contain stored
+      generated columns as well. If the column list is omitted, the publication
+      will replicate all non-generated columns (including any added in the
+      future) by default. Stored generated columns can also be replicated if
+      <literal>publish_generated_columns</literal> is set to
+      <literal>stored</literal>. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands. See
       <xref linkend="logical-replication-col-lists"/> for details about column
       lists.
      </para>
 
+     <para>
+      When a column list is specified with <literal>EXCEPT</literal>, the named
+      columns are not replicated. Specifying a column list has no effect on
+      <literal>TRUNCATE</literal> commands.
+     </para>
+
      <para>
       Only persistent base tables and partitioned tables can be part of a
       publication.  Temporary tables, unlogged tables, foreign tables,
@@ -371,10 +378,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
   </para>
 
   <para>
-   Any column list must include the <literal>REPLICA IDENTITY</literal> columns
-   in order for <command>UPDATE</command> or <command>DELETE</command>
-   operations to be published. There are no column list restrictions if the
-   publication publishes only <command>INSERT</command> operations.
+   In order for <command>UPDATE</command> or <command>DELETE</command>
+   operations to work, all the <literal>REPLICA IDENTITY</literal> columns must
+   be published. So, any column list must name all
+   <literal>REPLICA IDENTITY</literal> columns, and any
+   <literal>EXCEPT</literal> column list must not name any
+   <literal>REPLICA IDENTITY</literal> columns.
   </para>
 
   <para>
@@ -397,6 +406,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    to be published.
   </para>
 
+  <para>
+   The generated columns that are part of the column list specified with the
+   <literal>EXCEPT</literal> clause are not published, regardless of the
+   <literal>publish_generated_columns</literal> option. However, generated
+   columns that are not part of the column list specified with the
+   <literal>EXCEPT</literal> clause are published according to the value of the
+   <literal>publish_generated_columns</literal> option. See
+   <xref linkend="logical-replication-gencols"/> for details.
+  </para>
+
   <para>
    The row filter on a table becomes redundant if
    <literal>FOR TABLES IN SCHEMA</literal> is specified and the table
@@ -518,6 +537,15 @@ CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
 CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname);
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes for table
+   <structname>users</structname> except changes for column
+   <structname>security_pin</structname>:
+<programlisting>
+CREATE PUBLICATION users_safe FOR TABLE users EXCEPT (security_pin);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all sequences for synchronization:
 <programlisting>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 39c2cc2bf43..a124b5f6cec 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -266,14 +266,19 @@ is_schema_publication(Oid pubid)
  * If a column list is found, the corresponding bitmap is returned through the
  * cols parameter, if provided. The bitmap is constructed within the given
  * memory context (mcxt).
+ *
+ * If a column list is found specified with EXCEPT clause, except_columns is set
+ * to true.
  */
 bool
 check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
-							Bitmapset **cols)
+							Bitmapset **cols, bool *except_columns)
 {
 	HeapTuple	cftuple;
 	bool		found = false;
 
+	*except_columns = false;
+
 	if (pub->alltables)
 		return false;
 
@@ -299,6 +304,16 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
 			found = true;
 		}
 
+		/* Lookup the except attribute */
+		cfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, cftuple,
+								  Anum_pg_publication_rel_prexcept, &isnull);
+
+		if (!isnull)
+		{
+			Assert(!pub->alltables);
+			*except_columns = DatumGetBool(cfdatum);
+		}
+
 		ReleaseSysCache(cftuple);
 	}
 
@@ -660,10 +675,12 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
  * Returns a bitmap representing the columns of the specified table.
  *
  * Generated columns are included if include_gencols_type is
- * PUBLISH_GENCOLS_STORED.
+ * PUBLISH_GENCOLS_STORED. Columns that are in the except_cols are excluded from
+ * the column list.
  */
 Bitmapset *
-pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
+pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type,
+				  Bitmapset *except_cols)
 {
 	Bitmapset  *result = NULL;
 	TupleDesc	desc = RelationGetDescr(relation);
@@ -686,6 +703,9 @@ pub_form_cols_map(Relation relation, PublishGencolsType include_gencols_type)
 				continue;
 		}
 
+		if (except_cols && bms_is_member(att->attnum, except_cols))
+			continue;
+
 		result = bms_add_member(result, att->attnum);
 	}
 
@@ -790,8 +810,10 @@ GetRelationPublications(Oid relid, bool except_flag)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
 		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		bool		is_except_table = ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept &&
+			heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+		if (except_flag == is_except_table)
 			result = lappend_oid(result, pubid);
 	}
 
@@ -831,10 +853,12 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
 	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_publication_rel pubrel;
+		bool		has_collist = false;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		has_collist = !heap_attisnull(tup, Anum_pg_publication_rel_prattrs, NULL);
 
-		if (except_flag == pubrel->prexcept)
+		if (except_flag == (pubrel->prexcept && !has_collist))
 			result = GetPubPartitionOptionRelations(result, pub_partopt,
 													pubrel->prrelid);
 
@@ -1291,6 +1315,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		Oid			schemaid = get_rel_namespace(relid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+		Bitmapset  *except_columns = NULL;
 
 		/*
 		 * Form tuple with appropriate data.
@@ -1315,11 +1340,29 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		if (HeapTupleIsValid(pubtuple))
 		{
-			/* Lookup the column list attribute. */
 			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prattrs,
 										&(nulls[2]));
 
+			if (!nulls[2])
+			{
+				Datum		exceptDatum;
+				bool		isnull;
+
+				/*
+				 * We fetch pubtuple if publication is not FOR ALL TABLES and
+				 * not FOR TABLES IN SCHEMA. So if prexcept is true, it
+				 * indicates that prattrs contains columns to be excluded for
+				 * replication.
+				 */
+				exceptDatum = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+											  Anum_pg_publication_rel_prexcept,
+											  &isnull);
+
+				if (!isnull && DatumGetBool(exceptDatum))
+					except_columns = pub_collist_to_bitmapset(NULL, values[2], NULL);
+			}
+
 			/* Null indicates no filter. */
 			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
 										Anum_pg_publication_rel_prqual,
@@ -1331,8 +1374,12 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			nulls[3] = true;
 		}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
+		/*
+		 * Construct column list to show all columns when no column list is
+		 * specified or to show remaining columns when a column list is
+		 * provided with EXCEPT.
+		 */
+		if (except_columns || nulls[2])
 		{
 			Relation	rel = table_open(relid, AccessShareLock);
 			int			nattnums = 0;
@@ -1363,6 +1410,13 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 						continue;
 				}
 
+				/*
+				 * Skip columns that are part of column list specified with
+				 * EXCEPT.
+				 */
+				if (except_columns && bms_is_member(att->attnum, except_columns))
+					continue;
+
 				attnums[nattnums++] = att->attnum;
 			}
 
@@ -1371,6 +1425,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
 				nulls[2] = false;
 			}
+			else
+			{
+				values[2] = (Datum) 0;
+				nulls[2] = true;
+			}
 
 			table_close(rel, AccessShareLock);
 		}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 9a17dfc9d35..fa56797d7f7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -227,7 +227,6 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
-				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_EXCEPT_TABLE:
@@ -381,8 +380,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  * This function evaluates two conditions:
  *
  * 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
- *    by the column list. If any column is missing, *invalid_column_list is set
- *    to true.
+ *    by the column list and are not part of the column list specified with
+ *    EXCEPT. If any column is missing, *invalid_column_list is set to true.
  * 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
  *    are published, either by being explicitly named in the column list or, if
  *    no column list is specified, by setting the option
@@ -404,6 +403,7 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Publication *pub;
 	int			x;
+	bool		except_columns = false;
 
 	*invalid_column_list = false;
 	*invalid_gen_col = false;
@@ -427,7 +427,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 
 	/* Fetch the column list */
 	pub = GetPublication(pubid);
-	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
+	check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns,
+								&except_columns);
 
 	if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
 	{
@@ -517,8 +518,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			attnum = get_attnum(publish_as_relid, colname);
 		}
 
-		/* replica identity column, not covered by the column list */
-		*invalid_column_list |= !bms_is_member(attnum, columns);
+		/*
+		 * Replica identity column, not covered by the column list or is part
+		 * of column list specified with EXCEPT.
+		 */
+		if (except_columns)
+			*invalid_column_list |= bms_is_member(attnum, columns);
+		else
+			*invalid_column_list |= !bms_is_member(attnum, columns);
 
 		if (*invalid_column_list && *invalid_gen_col)
 			break;
@@ -1500,6 +1507,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			HeapTuple	rftuple;
 			Node	   *oldrelwhereclause = NULL;
 			Bitmapset  *oldcolumns = NULL;
+			bool		oldexcept = false;
 
 			/* look up the cache for the old relmap */
 			rftuple = SearchSysCache2(PUBLICATIONRELMAP,
@@ -1513,23 +1521,28 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 			if (HeapTupleIsValid(rftuple))
 			{
 				bool		isnull = true;
-				Datum		whereClauseDatum;
-				Datum		columnListDatum;
+				Datum		datum;
 
 				/* Load the WHERE clause for this table. */
-				whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												   Anum_pg_publication_rel_prqual,
-												   &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prqual,
+										&isnull);
 				if (!isnull)
-					oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum));
+					oldrelwhereclause = stringToNode(TextDatumGetCString(datum));
 
 				/* Transform the int2vector column list to a bitmap. */
-				columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
-												  Anum_pg_publication_rel_prattrs,
-												  &isnull);
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prattrs,
+										&isnull);
+				if (!isnull)
+					oldcolumns = pub_collist_to_bitmapset(NULL, datum, NULL);
 
+				/* Load the prexcept flag for this table. */
+				datum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple,
+										Anum_pg_publication_rel_prexcept,
+										&isnull);
 				if (!isnull)
-					oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL);
+					oldexcept = DatumGetBool(datum);
 
 				ReleaseSysCache(rftuple);
 			}
@@ -1556,13 +1569,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				 * Check if any of the new set of relations matches with the
 				 * existing relations in the publication. Additionally, if the
 				 * relation has an associated WHERE clause, check the WHERE
-				 * expressions also match. Same for the column list. Drop the
-				 * rest.
+				 * expressions also match. Same for the column list and except
+				 * flag. Drop the rest.
 				 */
 				if (newrelid == oldrelid)
 				{
 					if (equal(oldrelwhereclause, newpubrel->whereClause) &&
-						bms_equal(oldcolumns, newcolumns))
+						bms_equal(oldcolumns, newcolumns) &&
+						oldexcept == newpubrel->except)
 					{
 						found = true;
 						break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2ae51e5bfe1..774dfebdfa5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				OptWhereClause operator_def_arg
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality opt_without_overlaps
+%type <boolean> opt_ordinality opt_without_overlaps opt_except
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4480,6 +4480,11 @@ opt_without_overlaps:
 			| /*EMPTY*/								{ $$ = false; }
 	;
 
+opt_except:
+			EXCEPT									{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -10796,14 +10801,15 @@ CreatePublicationStmt:
  * relation_expr here.
  */
 PublicationObjSpec:
-			TABLE relation_expr opt_column_list OptWhereClause
+			TABLE relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_TABLE;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $2;
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
@@ -10819,7 +10825,7 @@ PublicationObjSpec:
 					$$->pubobjtype = PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA;
 					$$->location = @4;
 				}
-			| ColId opt_column_list OptWhereClause
+			| ColId opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
@@ -10827,7 +10833,7 @@ PublicationObjSpec:
 					 * If either a row filter or column list is specified, create
 					 * a PublicationTable object.
 					 */
-					if ($2 || $3)
+					if ($2 || $3 || $4)
 					{
 						/*
 						 * The OptWhereClause must be stored here but it is
@@ -10837,8 +10843,9 @@ PublicationObjSpec:
 						 */
 						$$->pubtable = makeNode(PublicationTable);
 						$$->pubtable->relation = makeRangeVar(NULL, $1, @1);
-						$$->pubtable->columns = $2;
-						$$->pubtable->whereClause = $3;
+						$$->pubtable->except = $2;
+						$$->pubtable->columns = $3;
+						$$->pubtable->whereClause = $4;
 					}
 					else
 					{
@@ -10846,25 +10853,27 @@ PublicationObjSpec:
 					}
 					$$->location = @1;
 				}
-			| ColId indirection opt_column_list OptWhereClause
+			| ColId indirection opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
-					$$->pubtable->columns = $3;
-					$$->pubtable->whereClause = $4;
+					$$->pubtable->except = $3;
+					$$->pubtable->columns = $4;
+					$$->pubtable->whereClause = $5;
 					$$->location = @1;
 				}
 			/* grammar like tablename * , ONLY tablename, ONLY ( tablename ) */
-			| extended_relation_expr opt_column_list OptWhereClause
+			| extended_relation_expr opt_except opt_column_list OptWhereClause
 				{
 					$$ = makeNode(PublicationObjSpec);
 					$$->pubobjtype = PUBLICATIONOBJ_CONTINUATION;
 					$$->pubtable = makeNode(PublicationTable);
 					$$->pubtable->relation = $1;
-					$$->pubtable->columns = $2;
-					$$->pubtable->whereClause = $3;
+					$$->pubtable->except = $2;
+					$$->pubtable->columns = $3;
+					$$->pubtable->whereClause = $4;
 				}
 			| CURRENT_SCHEMA
 				{
@@ -19856,6 +19865,13 @@ preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner)
 						errmsg("invalid table name"),
 						parser_errposition(pubobj->location));
 
+			if (pubobj->pubtable && pubobj->pubtable->except &&
+				pubobj->pubtable->columns == NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("table without column list cannot use EXCEPT clause"),
+						parser_errposition(pubobj->location));
+
 			if (pubobj->name)
 			{
 				/* convert it to PublicationTable */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 6bb0cbeedad..330248d1f7e 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -720,10 +720,18 @@ copy_read_data(void *outbuf, int minread, int maxread)
  * This function also returns (a) the relation qualifications to be used in
  * the COPY command, and (b) whether the remote relation has published any
  * generated column.
+ *
+ * With the introduction of the EXCEPT qualifier in column lists, it is now
+ * possible to define a publication that excludes all columns of a table. When
+ * the column list is fetched from the remote server and is NULL, it normally
+ * indicates that all columns are included. To distinguish this from the case
+ * where all columns are explicitly excluded, the 'all_cols_excluded' flag has
+ * been introduced.
  */
 static void
 fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
-						List **qual, bool *gencol_published)
+						List **qual, bool *gencol_published,
+						bool *all_cols_excluded)
 {
 	WalRcvExecResult *res;
 	StringInfoData cmd;
@@ -737,6 +745,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	Bitmapset  *included_cols = NULL;
 	int			server_version = walrcv_server_version(LogRepWorkerWalRcvConn);
 
+	Assert(*gencol_published == false);
+	Assert(*all_cols_excluded == false);
+
 	lrel->nspname = nspname;
 	lrel->relname = relname;
 
@@ -787,7 +798,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 	{
 		WalRcvExecResult *pubres;
 		TupleTableSlot *tslot;
-		Oid			attrsRow[] = {INT2VECTOROID};
+		Oid			attrsRow[] = {INT2VECTOROID, BOOLOID};
 
 		/* Build the pub_names comma-separated string. */
 		pub_names = makeStringInfo();
@@ -801,7 +812,17 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT"
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
+						 "   THEN NULL ELSE gpt.attrs END)");
+
+		/*
+		 * When publication is created with EXCEPT (column-list) and all
+		 * columns are specified, gpt.attrs will be NULL and no columns are
+		 * published in this case.
+		 */
+		if (server_version >= 190000)
+			appendStringInfo(&cmd, ", gpt.attrs IS NULL AND c.relnatts > 0");
+
+		appendStringInfo(&cmd,
 						 "  FROM pg_publication p,"
 						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
 						 "  pg_class c"
@@ -811,7 +832,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
-							 lengthof(attrsRow), attrsRow);
+							 server_version >= 190000 ? 2 : 1, attrsRow);
 
 		if (pubres->status != WALRCV_OK_TUPLES)
 			ereport(ERROR,
@@ -858,6 +879,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					included_cols = bms_add_member(included_cols, elems[natt]);
 			}
 
+			if (server_version >= 190000)
+				*all_cols_excluded = DatumGetBool(slot_getattr(tslot, 2, &isnull));
+
 			ExecClearTuple(tslot);
 		}
 		ExecDropSingleTupleTableSlot(tslot);
@@ -920,7 +944,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		Assert(!isnull);
 
 		/* If the column is not in the column list, skip it. */
-		if (included_cols != NULL && !bms_is_member(attnum, included_cols))
+		if (*all_cols_excluded ||
+			(included_cols != NULL && !bms_is_member(attnum, included_cols)))
 		{
 			ExecClearTuple(slot);
 			continue;
@@ -1052,11 +1077,15 @@ copy_table(Relation rel)
 	ParseState *pstate;
 	List	   *options = NIL;
 	bool		gencol_published = false;
+	bool		all_cols_excluded = false;
 
 	/* Get the publisher relation info. */
 	fetch_remote_table_info(get_namespace_name(RelationGetNamespace(rel)),
 							RelationGetRelationName(rel), &lrel, &qual,
-							&gencol_published);
+							&gencol_published, &all_cols_excluded);
+
+	if (all_cols_excluded)
+		return;
 
 	/* Put the relation into relmap. */
 	logicalrep_relmap_update(&lrel);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index a9593c5d9da..7f534618cf4 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -185,6 +185,16 @@ typedef struct RelationSyncEntry
 	 * row filter expressions, column list, etc.
 	 */
 	MemoryContext entry_cxt;
+
+	/*
+	 * Indicates whether no columns are published for a given relation. With
+	 * the introduction of the EXCEPT qualifier in column lists, it is now
+	 * possible to define a publication that excludes all columns of a table.
+	 * However, the 'columns' attribute cannot represent this case, since a
+	 * NULL value implies that all columns are published. To distinguish this
+	 * scenario, the 'all_cols_excluded' flag is introduced.
+	 */
+	bool		all_cols_excluded;
 } RelationSyncEntry;
 
 /*
@@ -1091,12 +1101,21 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	 */
 	foreach_ptr(Publication, pub, publications)
 	{
+		bool		has_column_list = false;
+		bool		except_columns = false;
+
+		has_column_list = check_and_fetch_column_list(pub,
+													  entry->publish_as_relid,
+													  NULL, NULL,
+													  &except_columns);
+
 		/*
 		 * The column list takes precedence over the
 		 * 'publish_generated_columns' parameter. Those will be checked later,
-		 * see pgoutput_column_list_init.
+		 * see pgoutput_column_list_init. But when a column list is specified
+		 * with EXCEPT, it should be checked.
 		 */
-		if (check_and_fetch_column_list(pub, entry->publish_as_relid, NULL, NULL))
+		if (has_column_list && !except_columns)
 			continue;
 
 		if (first)
@@ -1145,19 +1164,41 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 	{
 		Publication *pub = lfirst(lc);
 		Bitmapset  *cols = NULL;
+		bool		except_columns = false;
+		bool		all_cols_excluded = false;
 
 		/* Retrieve the bitmap of columns for a column list publication. */
 		found_pub_collist |= check_and_fetch_column_list(pub,
 														 entry->publish_as_relid,
-														 entry->entry_cxt, &cols);
+														 entry->entry_cxt, &cols,
+														 &except_columns);
+
+		/*
+		 * If column list is specified with EXCEPT retrieve bitmap of columns
+		 * which are not part of this column list.
+		 */
+		if (except_columns)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
+
+			cols = pub_form_cols_map(relation,
+									 entry->include_gencols_type, cols);
+			MemoryContextSwitchTo(oldcxt);
+
+			if (!cols)
+				all_cols_excluded = true;
+		}
 
 		/*
-		 * For non-column list publications — e.g. TABLE (without a column
-		 * list), ALL TABLES, or ALL TABLES IN SCHEMA, we consider all columns
-		 * of the table (including generated columns when
+		 * If 'cols' is null, it indicates that the publication is either a
+		 * non-column list publication or one where all columns are excluded.
+		 * When 'all_cols_excluded' is true, it explicitly means all columns
+		 * have been excluded. For non-column list publications — e.g. TABLE
+		 * (without a column list), ALL TABLES, or ALL TABLES IN SCHEMA, we
+		 * consider all columns of the table (including generated columns when
 		 * 'publish_generated_columns' parameter is true).
 		 */
-		if (!cols)
+		if (!all_cols_excluded && !cols)
 		{
 			/*
 			 * Cache the table columns for the first publication with no
@@ -1169,7 +1210,7 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 				MemoryContext oldcxt = MemoryContextSwitchTo(entry->entry_cxt);
 
 				relcols = pub_form_cols_map(relation,
-											entry->include_gencols_type);
+											entry->include_gencols_type, NULL);
 				MemoryContextSwitchTo(oldcxt);
 			}
 
@@ -1179,9 +1220,11 @@ pgoutput_column_list_init(PGOutputData *data, List *publications,
 		if (first)
 		{
 			entry->columns = cols;
+			entry->all_cols_excluded = all_cols_excluded;
 			first = false;
 		}
-		else if (!bms_equal(entry->columns, cols))
+		else if ((entry->all_cols_excluded != all_cols_excluded) ||
+				 !bms_equal(entry->columns, cols))
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot use different column lists for table \"%s.%s\" in different publications",
@@ -1505,6 +1548,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 
 	relentry = get_rel_sync_entry(data, relation);
 
+	/*
+	 * If all columns of a table are present in column list specified with
+	 * EXCEPT, skip publishing the changes.
+	 */
+	if (relentry->all_cols_excluded)
+		return;
+
 	/* First check the table filter */
 	switch (action)
 	{
@@ -2078,6 +2128,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->publish_as_relid = InvalidOid;
 		entry->columns = NULL;
 		entry->attrmap = NULL;
+		entry->all_cols_excluded = false;
 	}
 
 	/* Validate the entry */
@@ -2127,6 +2178,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		entry->pubactions.pubupdate = false;
 		entry->pubactions.pubdelete = false;
 		entry->pubactions.pubtruncate = false;
+		entry->all_cols_excluded = false;
 
 		/*
 		 * Tuple slots cleanups. (Will be rebuilt later if needed).
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f8250b000d8..5742a6f29b4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4933,24 +4933,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		if (tbinfo == NULL)
 			continue;
 
-		/* OK, make a DumpableObject for this relationship */
-		if (strcmp(prexcept, "f") == 0)
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
-		else
-			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
-
-		pubrinfo[j].dobj.catId.tableoid =
-			atooid(PQgetvalue(res, i, i_tableoid));
-		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
-		AssignDumpId(&pubrinfo[j].dobj);
-		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
-		pubrinfo[j].dobj.name = tbinfo->dobj.name;
-		pubrinfo[j].publication = pubinfo;
-		pubrinfo[j].pubtable = tbinfo;
-		if (PQgetisnull(res, i, i_prrelqual))
-			pubrinfo[j].pubrelqual = NULL;
-		else
-			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+		pubrinfo[j].pubexcept = (strcmp(prexcept, "t") == 0);
 
 		if (!PQgetisnull(res, i, i_prattrs))
 		{
@@ -4976,10 +4959,29 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		else
 			pubrinfo[j].pubrattrs = NULL;
 
+		/* OK, make a DumpableObject for this relationship */
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
+		pubrinfo[j].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&pubrinfo[j].dobj);
+		pubrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+		pubrinfo[j].dobj.name = tbinfo->dobj.name;
+		pubrinfo[j].publication = pubinfo;
+		pubrinfo[j].pubtable = tbinfo;
+		if (PQgetisnull(res, i, i_prrelqual))
+			pubrinfo[j].pubrelqual = NULL;
+		else
+			pubrinfo[j].pubrelqual = pg_strdup(PQgetvalue(res, i, i_prrelqual));
+
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
-		if (strcmp(prexcept, "t") == 0)
+		if (pubrinfo[j].pubexcept && !pubrinfo[j].pubrattrs)
 			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
 
 		j++;
@@ -5059,7 +5061,12 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 					  fmtQualifiedDumpable(tbinfo));
 
 	if (pubrinfo->pubrattrs)
+	{
+		if (pubrinfo->pubexcept)
+			appendPQExpBufferStr(query, " EXCEPT");
+
 		appendPQExpBuffer(query, " (%s)", pubrinfo->pubrattrs);
+	}
 
 	if (pubrinfo->pubrelqual)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 723b5575c53..ca2d356f72a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -690,6 +690,7 @@ typedef struct _PublicationRelInfo
 	TableInfo  *pubtable;
 	char	   *pubrelqual;
 	char	   *pubrattrs;
+	bool		pubexcept;
 } PublicationRelInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 50b1d435359..6ceb108a35b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1560,6 +1560,91 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * Add a footer to a publication description or a table description.
+ *
+ * 'is_pub_desc' - true for a pub desc; false for a table desc
+ * 'pub_schemas' - true if the pub_desc only shows schemas, otherwise false
+ */
+static bool
+addFooterToPublicationOrTableDesc(PQExpBuffer buf,
+								  printTableContent *const cont,
+								  const char *footermsg,
+								  bool is_pub_desc, bool pub_schemas)
+{
+	PGresult   *res;
+	int			count;
+	int			col = is_pub_desc ? 1 : 0;
+
+	res = PSQLexec(buf->data);
+	if (!res)
+		return false;
+
+	count = PQntuples(res);
+	if (count > 0)
+		printTableAddFooter(cont, footermsg);
+
+	/*--------------------------------------------------------------
+	 * Description columns for:
+	 *
+	 * PUB      TBL
+	 * [0]      -      : schema name (nspname)
+	 * [col]    -      : table name (relname)
+	 * -        [col]  : publication name (pubname)
+	 * [col+1]  [col+1]: row filter expression (prqual), may be NULL
+	 * [col+2]  [col+2]: column list (comma-separated), may be NULL
+	 * [col+3]  [col+3]: except flag ("t" if EXCEPT, else "f")
+	 *--------------------------------------------------------------
+	 */
+	for (int i = 0; i < count; i++)
+	{
+		printfPQExpBuffer(buf, "    "); /* indent */
+
+		/*
+		 * Footer entries for a publication description or a table
+		 * description
+		 */
+		if (is_pub_desc)
+		{
+			if (pub_schemas)
+			{
+				/* Schemas of the publication... */
+				appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, 0));
+			}
+			else
+			{
+				/* Tables of the publication... */
+				appendPQExpBuffer(buf, "\"%s.%s\"", PQgetvalue(res, i, 0),
+								  PQgetvalue(res, i, col));
+			}
+		}
+		else
+		{
+			/* Publications of the table... */
+			appendPQExpBuffer(buf, "\"%s\"", PQgetvalue(res, i, col));
+		}
+
+		/* Common footer output for column list and/or row filter */
+		if (!pub_schemas)
+		{
+			if (!PQgetisnull(res, i, col + 2))
+			{
+				if (strcmp(PQgetvalue(res, i, col + 3), "t") == 0)
+					appendPQExpBuffer(buf, " EXCEPT");
+				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, col + 2));
+			}
+
+			if (!PQgetisnull(res, i, col + 1))
+				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, col + 1));
+		}
+
+		printTableAddFooter(cont, buf->data);
+	}
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * describeOneTableDetails (for \d)
  *
@@ -3053,16 +3138,27 @@ describeOneTableDetails(const char *schemaname,
 		/* print any publications */
 		if (pset.sversion >= 100000)
 		{
-			if (pset.sversion >= 150000)
+			if (pset.sversion >= 190000)
 			{
 				printfPQExpBuffer(&buf,
+
+				/*
+				 * Get all publications for the schema that this relation is
+				 * part of
+				 */
 								  "SELECT pubname\n"
 								  "     , NULL\n"
 								  "     , NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+
+				/*
+				 * Get all publications for this relation created using FOR
+				 * TABLE
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3072,35 +3168,67 @@ describeOneTableDetails(const char *schemaname,
 								  "                pg_catalog.pg_attribute\n"
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
+								  "		, prexcept "
 								  "FROM pg_catalog.pg_publication p\n"
 								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
 								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n",
-								  oid, oid, oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+								  "WHERE pr.prrelid = '%s' "
+								  "AND	p.puballtables = false\n"
+								  "AND  c.relnamespace NOT IN (\n "
+								  " 	SELECT pnnspid FROM\n"
+								  " 	pg_catalog.pg_publication_namespace)\n"
 
-				appendPQExpBuffer(&buf,
+				/*
+				 * Get all FOR ALL TABLES publications that include this
+				 * relation
+				 */
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "		, NULL\n"
 								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
-								  oid);
-
-				if (pset.sversion >= 190000)
-					appendPQExpBuffer(&buf,
-									  "     AND NOT EXISTS (\n"
-									  "		SELECT 1\n"
-									  "		FROM pg_catalog.pg_publication_rel pr\n"
-									  "		JOIN pg_catalog.pg_class pc\n"
-									  "		ON pr.prrelid = pc.oid\n"
-									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
-									  oid);
-
-				appendPQExpBufferStr(&buf, "ORDER BY 1;");
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "     AND NOT EXISTS (\n"
+								  "		SELECT 1\n"
+								  "		FROM pg_catalog.pg_publication_rel pr\n"
+								  "		JOIN pg_catalog.pg_class pc\n"
+								  "		ON pr.prrelid = pc.oid\n"
+								  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid, oid);
+			}
+			else if (pset.sversion >= 150000)
+			{
+				printfPQExpBuffer(&buf,
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
+								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
+								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , pg_get_expr(pr.prqual, c.oid)\n"
+								  "     , (CASE WHEN pr.prattrs IS NOT NULL THEN\n"
+								  "         (SELECT string_agg(attname, ', ')\n"
+								  "           FROM pg_catalog.generate_series(0, pg_catalog.array_upper(pr.prattrs::pg_catalog.int2[], 1)) s,\n"
+								  "                pg_catalog.pg_attribute\n"
+								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
+								  "        ELSE NULL END) "
+								  "FROM pg_catalog.pg_publication p\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n"
+								  "UNION\n"
+								  "SELECT pubname\n"
+								  "     , NULL\n"
+								  "     , NULL\n"
+								  "FROM pg_catalog.pg_publication p\n"
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "ORDER BY 1;",
+								  oid, oid, oid, oid);
 			}
 			else
 			{
@@ -3121,34 +3249,8 @@ describeOneTableDetails(const char *schemaname,
 								  oid, oid);
 			}
 
-			result = PSQLexec(buf.data);
-			if (!result)
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Publications:"), false, false))
 				goto error_return;
-			else
-				tuples = PQntuples(result);
-
-			if (tuples > 0)
-				printTableAddFooter(&cont, _("Publications:"));
-
-			/* Might be an empty set - that's ok */
-			for (i = 0; i < tuples; i++)
-			{
-				printfPQExpBuffer(&buf, "    \"%s\"",
-								  PQgetvalue(result, i, 0));
-
-				/* column list (if any) */
-				if (!PQgetisnull(result, i, 2))
-					appendPQExpBuffer(&buf, " (%s)",
-									  PQgetvalue(result, i, 2));
-
-				/* row filter (if any) */
-				if (!PQgetisnull(result, i, 1))
-					appendPQExpBuffer(&buf, " WHERE %s",
-									  PQgetvalue(result, i, 1));
-
-				printTableAddFooter(&cont, buf.data);
-			}
-			PQclear(result);
 		}
 
 		/*
@@ -6532,49 +6634,6 @@ listPublications(const char *pattern)
 	return true;
 }
 
-/*
- * Add footer to publication description.
- */
-static bool
-addFooterToPublicationDesc(PQExpBuffer buf, const char *footermsg,
-						   bool as_schema, printTableContent *const cont)
-{
-	PGresult   *res;
-	int			count = 0;
-	int			i = 0;
-
-	res = PSQLexec(buf->data);
-	if (!res)
-		return false;
-	else
-		count = PQntuples(res);
-
-	if (count > 0)
-		printTableAddFooter(cont, footermsg);
-
-	for (i = 0; i < count; i++)
-	{
-		if (as_schema)
-			printfPQExpBuffer(buf, "    \"%s\"", PQgetvalue(res, i, 0));
-		else
-		{
-			printfPQExpBuffer(buf, "    \"%s.%s\"", PQgetvalue(res, i, 0),
-							  PQgetvalue(res, i, 1));
-
-			if (!PQgetisnull(res, i, 3))
-				appendPQExpBuffer(buf, " (%s)", PQgetvalue(res, i, 3));
-
-			if (!PQgetisnull(res, i, 2))
-				appendPQExpBuffer(buf, " WHERE %s", PQgetvalue(res, i, 2));
-		}
-
-		printTableAddFooter(cont, buf->data);
-	}
-
-	PQclear(res);
-	return true;
-}
-
 /*
  * \dRp+
  * Describes publications including the contents.
@@ -6764,6 +6823,12 @@ describePublications(const char *pattern)
 			else
 				appendPQExpBufferStr(&buf,
 									 ", NULL, NULL");
+
+			if (pset.sversion >= 190000)
+				appendPQExpBufferStr(&buf, ", prexcept");
+			else
+				appendPQExpBufferStr(&buf, ", NULL");
+
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c,\n"
 							  "     pg_catalog.pg_namespace n,\n"
@@ -6772,11 +6837,8 @@ describePublications(const char *pattern)
 							  "  AND c.oid = pr.prrelid\n"
 							  "  AND pr.prpubid = '%s'\n", pubid);
 
-			if (pset.sversion >= 190000)
-				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
-
 			appendPQExpBuffer(&buf, "ORDER BY 1,2");
-			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
+			if (!addFooterToPublicationOrTableDesc(&buf, &cont, _("Tables:"), true, false))
 				goto error_return;
 
 			if (pset.sversion >= 150000)
@@ -6788,8 +6850,8 @@ describePublications(const char *pattern)
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n"
 								  "WHERE pn.pnpubid = '%s'\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Tables from schemas:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Tables from schemas:"), true, true))
 					goto error_return;
 			}
 		}
@@ -6799,14 +6861,14 @@ describePublications(const char *pattern)
 			{
 				/* Get the excluded tables for the specified publication */
 				printfPQExpBuffer(&buf,
-								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "SELECT c.relnamespace::regnamespace, c.relname, NULL, NULL\n"
 								  "FROM pg_catalog.pg_class c\n"
 								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
 								  "WHERE pr.prpubid = '%s'\n"
 								  "  AND pr.prexcept\n"
 								  "ORDER BY 1", pubid);
-				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
-												true, &cont))
+				if (!addFooterToPublicationOrTableDesc(&buf, &cont,
+													   _("Except tables:"), true, false))
 					goto error_return;
 			}
 		}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 63036ec7656..9d9221d0419 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2295,6 +2295,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES"))
 		COMPLETE_WITH("EXCEPT TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT (");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT"))
+		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "ALL", "TABLES", "EXCEPT", "TABLE"))
@@ -2316,10 +2320,13 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("(");
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "WHERE", "("))
 		COMPLETE_WITH_ATTR(prev3_wd);
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "EXCEPT", "("))
+		COMPLETE_WITH_ATTR(prev3_wd);
 	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-			 !TailMatches("WHERE", "(*)"))
+			 !TailMatches("WHERE", "(*)") && !TailMatches("EXCEPT", "("))
 		COMPLETE_WITH(",", "WHERE (");
-	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+			 !ends_with(prev_wd, '('))
 		COMPLETE_WITH(",");
 	/* ALTER PUBLICATION <name> DROP */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "DROP"))
@@ -3637,7 +3644,7 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
-		COMPLETE_WITH("WHERE (", "WITH (");
+		COMPLETE_WITH("EXCEPT (", "WHERE (", "WITH (");
 	/* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index c7a61f3194c..481bb6e5ca9 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -196,7 +196,8 @@ extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
-										MemoryContext mcxt, Bitmapset **cols);
+										MemoryContext mcxt, Bitmapset **cols,
+										bool *except_columns);
 extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 											  bool if_not_exists);
 extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns);
@@ -206,6 +207,7 @@ extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid,
 extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
 										   MemoryContext mcxt);
 extern Bitmapset *pub_form_cols_map(Relation relation,
-									PublishGencolsType include_gencols_type);
+									PublishGencolsType include_gencols_type,
+									Bitmapset *except_cols);
 
 #endif							/* PG_PUBLICATION_H */
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index e7d7f3ba85c..6a2168fc32c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,11 +31,12 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
-	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation or columns */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
-	int2vector	prattrs;		/* columns to replicate */
+	int2vector	prattrs;		/* columns to replicate or exclude to
+								 * replicate */
 #endif
 } FormData_pg_publication_rel;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 06b54d8c834..8f6ce67158d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2183,6 +2183,94 @@ Except tables:
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+    pubname     | schemaname |    tablename     | attnames  | rowfilter 
+----------------+------------+------------------+-----------+-----------
+ testpub_except | public     | pub_test_except1 | {a,b,c,d} | 
+ testpub_except | pub_sch1   | pub_test_except2 | {a,d}     | 
+(2 rows)
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+ERROR:  cannot use column list for relation "public.pub_test_except1" in publication "testpub_except2"
+DETAIL:  Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+ERROR:  table without column list cannot use EXCEPT clause
+LINE 1: CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except...
+                                               ^
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (a, b)
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+ERROR:  column list must not be specified in ALTER PUBLICATION ... DROP
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+                                                   Publication testpub_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
+Tables:
+    "pub_sch1.pub_test_except2"
+    "public.pub_test_except1" EXCEPT (c, d)
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+ERROR:  cannot update table "pub_test_except1"
+DETAIL:  Column list used by the publication does not cover the replica identity.
+DROP INDEX pub_test_except1_ac_idx;
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+                             Table "public.pub_test_except1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+ d      | integer |           |          |         | plain   |              | 
+Indexes:
+    "pub_test_except1_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Publications:
+    "testpub_except" EXCEPT (c, d)
+Not-null constraints:
+    "pub_test_except1_a_not_null" NOT NULL "a"
+    "pub_test_except1_c_not_null" NOT NULL "c"
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 -- ======================================================
 -- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 292deb52b93..a7a11765aa9 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1363,6 +1363,60 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE (pub_sch1.tbl1, pub_
 DROP PUBLICATION testpub_reset;
 DROP TABLE pub_sch1.tbl1;
 DROP TABLE pub_sch1.tbl2;
+-- ======================================================
+
+-- Test EXCEPT columns for CREATE PUBLICATION
+SET client_min_messages = 'ERROR';
+CREATE TABLE pub_test_except1 (a int NOT NULL, b int, c int NOT NULL, d int);
+CREATE TABLE pub_sch1.pub_test_except2 (a int, b int, c int, d int);
+
+-- Verify that publication is created with EXCEPT
+CREATE PUBLICATION testpub_except FOR TABLE pub_test_except1, pub_sch1.pub_test_except2 EXCEPT (b, c);
+SELECT * FROM pg_publication_tables WHERE pubname = 'testpub_except';
+
+-- Cannot use EXCEPT col-lists combined with TABLES IN SCHEMA
+CREATE PUBLICATION testpub_except2 FOR TABLES IN SCHEMA pub_sch1, TABLE pub_test_except1 EXCEPT (b, c);
+
+-- Syntax error EXCEPT without a col-list
+CREATE PUBLICATION testpub_except2 FOR TABLE pub_test_except1 EXCEPT;
+
+-- Verify ok - ALTER PUBLICATION ... SET ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except SET TABLE pub_test_except1 EXCEPT (a, b), pub_sch1.pub_test_except2;
+\dRp+ testpub_except
+
+-- Verify fails - ALTER PUBLICATION ... DROP ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1 EXCEPT (a, b);
+
+-- Verify ok - ALTER PUBLICATION ... DROP
+ALTER PUBLICATION testpub_except DROP TABLE pub_test_except1;
+
+-- Verify ok - ALTER PUBLICATION ... ADD ... EXCEPT (col-list)
+ALTER PUBLICATION testpub_except ADD TABLE pub_test_except1 EXCEPT (c, d);
+\dRp+ testpub_except
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using RI FULL)
+ALTER TABLE pub_test_except1 REPLICA IDENTITY FULL;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify fails - EXCEPT col-list cannot contain RI cols (when using INDEX)
+CREATE UNIQUE INDEX pub_test_except1_ac_idx ON pub_test_except1 (a, c);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_ac_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+DROP INDEX pub_test_except1_ac_idx;
+
+-- Verify ok - no clash between RI cols and the EXCEPT col-list
+CREATE UNIQUE INDEX pub_test_except1_a_idx ON pub_test_except1 (a);
+ALTER TABLE pub_test_except1 REPLICA IDENTITY USING INDEX pub_test_except1_a_idx;
+UPDATE pub_test_except1 SET a = 3 WHERE a = 1;
+
+-- Verify description of a table with publication with EXCEPT col-list
+\d+ pub_test_except1
+
+-- cleanup
+DROP INDEX pub_test_except1_a_idx;
+DROP PUBLICATION testpub_except;
+DROP TABLE pub_test_except1;
+DROP TABLE pub_sch1.pub_test_except2;
 DROP SCHEMA pub_sch1;
 -- ======================================================
 
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index b8e5c54c314..e8e69f7443d 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -47,6 +47,7 @@ tests += {
       't/035_conflicts.pl',
       't/036_sequences.pl',
       't/037_rep_changes_except_table.pl',
+      't/038_rep_changes_except_collist.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/038_rep_changes_except_collist.pl b/src/test/subscription/t/038_rep_changes_except_collist.pl
new file mode 100644
index 00000000000..3dfd266bc3d
--- /dev/null
+++ b/src/test/subscription/t/038_rep_changes_except_collist.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT (column-list) publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# Initial setup
+$node_publisher->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab4 (a int, bgen int GENERATED ALWAYS AS (a * 2) STORED, cgen int GENERATED ALWAYS AS (a * 3) STORED);
+	CREATE TABLE tab5 (a int, b int, c int);
+	INSERT INTO tab1 VALUES (1, 2, 3);
+	INSERT INTO sch1.tab1 VALUES (1, 2, 3);
+	CREATE PUBLICATION tap_pub_col FOR TABLE tab1 EXCEPT (a), sch1.tab1 EXCEPT (b, c);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq (
+	CREATE SCHEMA sch1;
+	CREATE TABLE tab1 (a int, b int NOT NULL, c int);
+	CREATE TABLE sch1.tab1 (a int, b int, c int);
+	CREATE TABLE tab2 (a int, b int, c int);
+	CREATE TABLE tab3 (a int, bgen int, cgen int);
+	CREATE TABLE tab4 (a int, bgen int, cgen int);
+	CREATE TABLE tab5 (a int, b int, c int, d int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_col CONNECTION '$publisher_connstr' PUBLICATION tap_pub_col"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+# Test initial sync
+my $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
+is($result, qq(|2|3),
+	'Verify initial sync of tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.tab1");
+is($result, qq(1||),
+	'Verify initial sync of sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test incremental changes
+$node_publisher->safe_psql(
+	'postgres', qq (
+	INSERT INTO tab1 VALUES (4, 5, 6);
+	INSERT INTO sch1.tab1 VALUES (4, 5, 6);
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(|2|3
+|5|6),
+	'Verify incremental inserts on tab1 in a publication using EXCEPT (column-list)'
+);
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM sch1.tab1 ORDER BY a");
+is( $result, qq(1||
+4||),
+	'Verify incremental inserts on sch1.tab1 in a publication using EXCEPT (column-list)'
+);
+
+# Test for update
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+));
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE UNIQUE INDEX b_idx ON tab1 (b);
+	ALTER TABLE tab1 REPLICA IDENTITY USING INDEX b_idx;
+	UPDATE tab1 SET a = 991, b = 992, c = 993 WHERE a = 1;
+));
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1  ORDER BY a");
+is( $result, qq(|5|6
+|992|993),
+	'check update for EXCEPT (column-list) publication');
+
+# Test ALTER PUBLICATION for EXCEPT (column-list)
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION tap_pub_col ADD TABLE tab2 EXCEPT(b)");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab2 VALUES (1, 2, 3)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab2");
+is($result, qq(1||3), 'check alter publication with EXCEPT (column-list)');
+
+# Test for publication created with 'publish_generated_columns' as 'stored' on
+# table 'tab3' and with column 'bgen' in column list with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(INSERT INTO tab3 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = stored);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab3 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab3 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab3 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'stored', so data corresponding to column 'cgen' is replicated.
+is( $result, qq(1||3
+2||6),
+	'check publication(publish_generated_columns as stored) with generated columns and EXCEPT (column-list)'
+);
+
+# Test for publication created with 'publish_generated_columns' as 'none' on
+# table with generated columns and column list specified with EXCEPT clause.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO tab4 VALUES (1);
+	ALTER PUBLICATION tap_pub_col SET (publish_generated_columns = none);
+	ALTER PUBLICATION tap_pub_col SET TABLE tab4 EXCEPT(bgen);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab4 VALUES (2)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab4 ORDER BY a");
+
+# column 'bgen' is specified in EXCEPT (columm-list). So data corresponding to
+# 'bgen' is not replicated. Parameter 'publish_generated_columns' is set as
+# 'none', so data corresponding to column 'cgen' is not replicated.
+is( $result, qq(1||
+2||),
+	'check publication(publish_generated_columns as none) with generated columns and EXCEPT (column-list)'
+);
+
+# All columns are present in EXCEPT (column-list)
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER PUBLICATION tap_pub_col SET TABLE tab5 EXCEPT(a, b, c);
+	INSERT INTO tab5 VALUES (1, 2, 3);
+));
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub_col REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_col');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab5 VALUES (4, 5, 6)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(), 'all columns are specified in EXCEPT (column-list)');
+
+# Add a new column and check that it is replicated
+$node_publisher->safe_psql(
+	'postgres', qq(
+	ALTER TABLE tab5 ADD COLUMN d int;
+));
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab5 VALUES (7, 8, 9, 10)");
+$node_publisher->wait_for_catchup('tap_sub_col');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab5");
+is($result, qq(|||10), 'newly added column is replicated');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#152Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#151)
Re: Skipping schema changes in publication

Hi Shlok. Some review comments for v29-0001 (ALTER PUBLICATION RESET)

======
doc/src/sgml/ref/alter_publication.sgml

Description:

1.
   <para>
    The command <command>ALTER PUBLICATION</command> can change the attributes
-   of a publication.
-  </para>
+   of a publication. There are several subforms described below.

IMO, you don't need to say that sentence "There are several subforms
described below," because it is obvious from the context.

(I guess you copied it from ALTER TABLE, but that doesn't change my opinion)

~~~

<General>

2.
The new description list entries all say:

"This form adds..."
"This form replaces..."
"This for removes..."
etc.

IMO, the words "This form" are all unnecessary here. Instead, just say
"Adds...", "Replaces...", "Removes...", etc.

(I guess you copied it from ALTER TABLE, but that doesn't change my opinion)

~~~

DROP:

3.
+   <varlistentry>
+    <term><literal>DROP <replaceable
class="parameter">publication_object</replaceable> [,
...]</literal></term>
+    <listitem>

The replacement name here should be "publication_drop_object".

~~~

SET:

4.
+     <para>
+      This form can change all of the publication properties specified in
+      <xref linkend="sql-createpublication"/>. Properties not mentioned in the
+      command retain their previous settings. It is not applicable to
+      sequences.
+     </para>

For "can change all of"; maybe that should be "can change any of".
Also, for "It is not applicable." -- saying "it" seemed awkward.

IMO the current master text (shown below) seemed OK as-is; At least I
thought it was better than the patch replacement text:
This clause alters publication parameters originally set by CREATE
PUBLICATION. See there for more information. This clause is not
applicable to sequences.

~~~

5.
+      <para>
+       This problem can be avoided by refraining from modifying partition leaf
+       tables after the <command>ALTER PUBLICATION ... SET</command> until the
+       <link linkend="sql-altersubscription"><command>ALTER
SUBSCRIPTION ... REFRESH PUBLICATION</command></link>
+       is executed and by only refreshing using the
<literal>copy_data = off</literal>
+       option.
+      </para>

This link would be better if it referenced the actual REFRESH
PUBLICATION. Currently, it just goes to the top of the ALTER
SUBSCRIPTION page.

~~~

6.
   <para>
    You must own the publication to use <command>ALTER PUBLICATION</command>.
    Adding a table to a publication additionally requires owning that table.
-   The <literal>ADD TABLES IN SCHEMA</literal> and
-   <literal>SET TABLES IN SCHEMA</literal> to a publication requires the
-   invoking user to be a superuser.
-   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
-   new owning role, and that role must have <literal>CREATE</literal>
-   privilege on the database.
+   The <literal>ADD TABLES IN SCHEMA</literal>,
+   <literal>SET TABLES IN SCHEMA</literal> to a publication and
+   <literal>RESET</literal> of publication requires the invoking user to be a
+   superuser. To alter the owner, you must be able to
+   <literal>SET ROLE</literal> to the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the database.
    Also, the new owner of a
    <link linkend="sql-createpublication-params-for-tables-in-schema"><literal>FOR
TABLES IN SCHEMA</literal></link>
    or <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR
ALL TABLES</literal></link>

I felt that those sentences:
"To alter the owner, you must ...", and
"Also, the new owner of a ..."
really belonged in the ALTER OWNER TO description part.

src/test/regress/sql/publication.sql
======

7.
+ALTER PUBLICATION testpub_reset SET (publish_via_partition_root = 'true');

It's a bit strange to say 'true' in quotes. Indeed, you shouldn't need
to give any value here -- just say "SET (publish_via_partition_root)"

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

#153shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#151)
Re: Skipping schema changes in publication

On Thu, Dec 4, 2025 at 5:21 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have addressed these in the v29 patch.
Will address comments for 0003 and 0004 patch by Peter and comments by
Shveta in next version.

Thanks for the patch.

I believe patch 003 (EXCEPT table) and 004 (EXCEPT column_list) should
be the primary focus. The other patches, RESET and ADD ALL tables are
additional features and may or may not be necessary depending on
further input.

I suggest making 003 and 004 as 001 and 002 respectively, and
prioritizing these first. The RESET and ADD ALL tables patches can
become 003 and 004, and we can review them after the first 2 patches
are fully evaluated. The documentation formatting changes for ALTER
PUBLICATION are not required in this patch and, if needed, can be
handled separately.

thanks
Shveta

#154Amit Kapila
amit.kapila16@gmail.com
In reply to: shveta malik (#153)
Re: Skipping schema changes in publication

On Mon, Dec 8, 2025 at 5:27 PM shveta malik <shveta.malik@gmail.com> wrote:

On Thu, Dec 4, 2025 at 5:21 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have addressed these in the v29 patch.
Will address comments for 0003 and 0004 patch by Peter and comments by
Shveta in next version.

Thanks for the patch.

I believe patch 003 (EXCEPT table) and 004 (EXCEPT column_list) should
be the primary focus.

+1. We should first try to make 0003 RFC before going further.

--
With Regards,
Amit Kapila.

#155Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Amit Kapila (#154)
1 attachment(s)
Re: Skipping schema changes in publication

On Mon, 8 Dec 2025 at 17:44, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, Dec 8, 2025 at 5:27 PM shveta malik <shveta.malik@gmail.com> wrote:

On Thu, Dec 4, 2025 at 5:21 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have addressed these in the v29 patch.
Will address comments for 0003 and 0004 patch by Peter and comments by
Shveta in next version.

Thanks for the patch.

I believe patch 003 (EXCEPT table) and 004 (EXCEPT column_list) should
be the primary focus.

+1. We should first try to make 0003 RFC before going further.

I have removed the 0001 0002 and 0004 patches for now. Will post them
once 0003 patch is RFC.
Here is the update patch for "EXCEPT TABLE".

Thanks,
Shlok Kyal

Attachments:

v30-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v30-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 933ab54e8e8ebd8db0bd691b7f48bdc2dd6fdc85 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Dec 2025 22:41:23 +0530
Subject: [PATCH v30] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  47 +++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          | 122 +++++++---
 src/backend/commands/publicationcmds.c        | 105 ++++++---
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  34 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  27 +--
 src/backend/utils/cache/relcache.c            |  24 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  10 +-
 src/bin/pg_dump/t/002_pg_dump.pl              |  22 +-
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   5 +
 src/test/regress/expected/publication.out     |  57 ++++-
 src/test/regress/sql/publication.sql          |  21 +-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 215 ++++++++++++++++++
 24 files changed, 736 insertions(+), 123 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..a4d32de58ec 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index aa013f348d4..c420469feaa 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2550,10 +2550,12 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..1280837f995 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +190,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>
+     <para>
+      The partitioned table or its partitions are excluded from the publication
+      based on the parameter <literal>publish_via_partition_root</literal>.
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect, as changes are replicated via the leaf
+      partitions. Specifying a leaf partition excludes only that partition from
+      replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -467,6 +502,14 @@ CREATE PUBLICATION production_publication FOR TABLE users, departments, TABLES I
 CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales;
 </programlisting></para>
 
+  <para>
+   Create a publication that publishes all changes in all the tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION mypublication FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
   <para>
    Create a publication that publishes all changes for table <structname>users</structname>,
    but replicates only columns <structname>user_id</structname> and
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 0994220c53d..64b738f7d34 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,43 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = NIL;
+		List	   *aexceptpubids = NIL;
+		List	   *aschemapubids = NIL;
+		bool		set_top = false;
+
+		GetRelationPublications(ancestor, &apubids, &aexceptpubids);
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+				set_top = !list_member_oid(aexceptpubids, puboid);
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +478,16 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check when a partition is excluded via EXCEPT TABLE while the
+	 * publication has publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+		pub->pubviaroot)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" will be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +504,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,35 +773,59 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
-List *
-GetRelationPublications(Oid relid)
+/*
+ * Get the list of publication oids associated with a specified relation.
+ * pubids is filled with the list of publication oids the relation is part of.
+ * except_pubids is filled with the list of publication oids the relation is
+ * excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */
+bool
+GetRelationPublications(Oid relid, List **pubids, List **except_pubids)
 {
-	List	   *result = NIL;
 	CatCList   *pubrellist;
-	int			i;
+	bool		found = false;
 
 	/* Find all publications associated with the relation. */
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
-		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		Oid			pubid = pubrel->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (pubrel->prexcept)
+		{
+			if (except_pubids)
+				*except_pubids = lappend_oid(*except_pubids, pubid);
+		}
+		else
+		{
+			if (pubids)
+				*pubids = lappend_oid(*pubids, pubid);
+			found = true;
+		}
 	}
 
 	ReleaseSysCacheList(pubrellist);
 
-	return result;
+	return found;
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR ALL TABLES publication, this returns the list of tables that were
+ * explicitly excluded via an EXCEPT TABLE clause.
  *
- * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * For a FOR TABLE publication, this returns the list of tables explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
  */
 List *
 GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
@@ -866,13 +914,18 @@ GetAllTablesPublications(void)
  * publication.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +944,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +966,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1223,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1367,7 +1423,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 1faf3a8c372..402100640d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,38 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +226,8 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = (pubobj->pubobjtype == PUBLICATIONOBJ_EXCEPT_TABLE);
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +302,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +329,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -354,8 +389,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool pubviaroot,
+							char pubgencols_type, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +414,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -923,16 +959,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
 	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
@@ -944,22 +973,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
-
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
-
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
-
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
-
 		if (schemaidlist != NIL)
 		{
 			/*
@@ -971,8 +984,37 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 		}
 	}
 
+	/*
+	 * If publication is for ALL TABLES and relations is not empty, it means
+	 * that there are some relations to be excluded from the publication.
+	 * Else, relations is the list of relations to be added to the
+	 * publication.
+	 */
+	if (relations != NIL)
+	{
+		List	   *rels;
+
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
+
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
+
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
+
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1348,6 +1390,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc(sizeof(PublicationRelInfo));
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1761,6 +1804,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1833,6 +1877,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 07e5b95782e..a35f40a1956 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3a0a354a9c..89429e63d8b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				pub_except_obj_list opt_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> PublicationExceptObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10761,6 +10763,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10801,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10877,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10897,6 +10904,31 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+/*
+ * ALL TABLES EXCEPT ( table_name [, ...] ) specification
+ */
+PublicationExceptObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+pub_except_obj_list: PublicationExceptObjSpec
+					{ $$ = list_make1($1); }
+			| pub_except_obj_list ',' PublicationExceptObjSpec
+					{ $$ = lappend($1, $3); }
+	;
+
+opt_except_clause:
+			EXCEPT opt_table '(' pub_except_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
 
 /*****************************************************************************
  *
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 942e1abdb58..fbae7bcc464 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = NIL;
+		List	   *exceptTablePubids = NIL;
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2099,6 +2100,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		char		relkind = get_rel_relkind(relid);
 		List	   *rel_publications = NIL;
 
+		GetRelationPublications(relid, &pubids, &exceptTablePubids);
+
 		/* Reload publications if needed before use. */
 		if (!publications_valid)
 		{
@@ -2195,22 +2198,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2216,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2232,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2312,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc9084..1b0f35582bb 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5793,7 +5793,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
-	List	   *puboids;
+	List	   *puboids = NIL;
+	List	   *exceptpuboids = NIL;
+	List	   *alltablespuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	GetRelationPublications(relid, &puboids, &exceptpuboids);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5843,16 +5845,25 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		foreach(lc, ancestors)
 		{
 			Oid			ancestor = lfirst_oid(lc);
+			List	   *ancestor_puboids = NIL;
+			List	   *ancestor_exceptpuboids = NIL;
 
-			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+			GetRelationPublications(ancestor, &ancestor_puboids,
+									&ancestor_exceptpuboids);
+
+			puboids = list_concat_unique_oid(puboids, ancestor_puboids);
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   ancestor_exceptpuboids);
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5894,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5899,6 +5910,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		 */
 		if ((pubform->pubupdate || pubform->pubdelete) &&
 			pub_contains_invalid_column(pubid, relation, ancestors,
+										pubform->puballtables,
 										pubform->pubviaroot,
 										pubform->pubgencols,
 										&invalid_column_list,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2445085dbbd..929ea0a9f87 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4662,7 +4664,33 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		bool first_tbl = true;
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has EXCEPT TABLEs */
+		for (SimplePtrListCell *cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first_tbl)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first_tbl = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first_tbl)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4831,6 +4859,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4842,8 +4871,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4854,6 +4891,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4869,6 +4907,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4880,6 +4919,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		bool		prexcept = strcmp(PQgetvalue(res, i, i_prexcept), "t") == 0;
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4893,7 +4933,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (prexcept)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4934,6 +4978,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (prexcept)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11812,6 +11859,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20182,6 +20232,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..6ebeb9c96a1 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -432,7 +434,8 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
-	else if (obj1->objType == DO_PUBLICATION_REL)
+	else if (obj1->objType == DO_PUBLICATION_REL ||
+			 obj1->objType == DO_PUBLICATION_EXCEPT_REL)
 	{
 		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
 		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
@@ -1715,6 +1718,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..09b164030d6 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,26 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
@@ -5163,7 +5183,7 @@ foreach my $run (sort keys %pgdump_runs)
 		#
 		# Either "all_runs" should be set or there should be a "like" list,
 		# even if it is empty.  (This makes the test more self-documenting.)
-		if (!defined($tests{$test}->{all_runs})
+		if (   !defined($tests{$test}->{all_runs})
 			&& !defined($tests{$test}->{like}))
 		{
 			die "missing \"like\" in test \"$test\"";
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 20d7a65c614..45e7a9cbfd3 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3623,7 +3623,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..c3a5e278a03 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern bool GetRelationPublications(Oid relid, List **pubids, List **except_pubids);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -170,7 +171,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -181,7 +182,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..4a170994f76 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
-										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										List *ancestors, bool puballtables,
+										bool pubviaroot, char pubgencols_type,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..a14ecedb27f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
@@ -4326,6 +4329,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_Reset,					/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
@@ -4341,6 +4345,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..ef469c761d0 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,40 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+-- EXCEPT with wildcard: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3*);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +265,34 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub7
+                                                      Publication testpub7
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..651b3b12030 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,37 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+-- EXCEPT with wildcard: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3*);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
+\dRp+ testpub7
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..09174b7d5d7
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,215 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT TABLE publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# ============================================
+# EXCEPT TABLE test cases for normal tables
+# ============================================
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Verify that data inserted to the excluded table is not replicated.
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+# ============================================
+# EXCEPT TABLE test cases for partition tables
+# ============================================
+# Check behavior of EXCEPT TABLE together with publish_via_partition_root
+# when applied to a partitioned table and its partitions.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+	CREATE TABLE sch1.part2(a int);
+));
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = false
+# Excluding a partition while publish_via_partition_root = false prevents
+# replication of rows inserted into the partitioned table.
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on other partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table while publish_via_partition_root = false
+# still allows rows inserted into its partitions to be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = true
+# When the partitioned table is excluded and publish_via_partition_root is true,
+# no rows from the table or its partitions are replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = true
+# When a partition is excluded but publish_via_partition_root is true,
+# rows published through the partitioned table can still be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1 ORDER BY a");
+is( $result, qq(1
+2
+6
+7), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on other partition');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#156Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#147)
Re: Skipping schema changes in publication

On Mon, 24 Nov 2025 at 13:03, Peter Smith <smithpb2250@gmail.com> wrote:

On Fri, Nov 21, 2025 at 5:55 PM Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Here are some review comments for your patch v28-0003 (EXCEPT TABLE ...).

The review of this patch is a WIP. In this post I only looked at the test code.

Here are my remaining review comments for patch v28-0003 (EXCEPT TABLE ...).

======
doc/src/sgml/ref/create_publication.sgml

1.
-ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES
+ALTER PUBLICATION <replaceable class="parameter">name</replaceable>
ADD ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable
class="parameter">table_exception_object</replaceable> [, ... ] ) ]

Why is that optional [TABLE] keyword needed?

I know PostGres commands sometimes have "noise" words in the syntax so
the command can be more English-like, but in this case, the
publication is a FOR ALL *TABLES* anyway, so I am not sure what the
benefit is of the user being able to say TABLE a 2nd time?

I think this feature can be extended to EXCEPT SCHEMA etc. So I think
it is necessary for clarity.
There is already a discussion [1]/messages/by-id/CAA4eK1JEKs8qwwhRb1BCiMNduJ5ePUtFnTscrZt86UKWBkLxwg@mail.gmail.com.

======
src/backend/catalog/pg_publication.c

2.
+ /*
+ * Check for partitions of partitioned table which are specified with
+ * EXCEPT clause and partitioned table is published with
+ * publish_via_partition_root = true.
+ */

I think you can just say "partitions" or "table partitions", but
"partitions of [a] partitioned table" seems overkill.

Also, "... and partitioned table is published with
publish_via_partition_root = true." seems too wordy. Isn't that just
the same as "... and publish_via_partition_root = true"

SUGGESTION
Check for when the publication says "EXCEPT TABLE (partition)" but
publish_via_partition_root = true.

~~~

Modified

3.
-/* Gets list of publication oids for a relation */
+/* Gets list of publication oids for a relation that matches the except_flag */
List *
-GetRelationPublications(Oid relid)
+GetRelationPublications(Oid relid, bool except_flag)
{
List    *result = NIL;
CatCList   *pubrellist;
@@ -765,7 +791,8 @@ GetRelationPublications(Oid relid)
HeapTuple tup = &pubrellist->members[i]->tuple;
Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
- result = lappend_oid(result, pubid);
+ if (except_flag == ((Form_pg_publication_rel) GETSTRUCT(tup))->prexcept)
+ result = lappend_oid(result, pubid);
}

I was wondering if it might be better to return 2 lists from this
function (e.g. an included-list, and an excluded-list) instead of
passing the 'except_flag' like the current code. IIUC, you are mostly
calling this function twice to get 2 lists anyway, but returning 2
lists instead of 1, this function might be more efficient since it
will only process the publication loop once.

Modified

~~~

4.
/*
* Gets list of relation oids for a publication that matches the except_flag.
*
* This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
* should use GetAllPublicationRelations().
*/
List *
GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
bool except_flag)
Something doesn't seem right -- the function comment says we shouldn't
be calling the function for FOR ALL TABLES, but meanwhile, EXCEPT
TABLE is currently only implemented via FOR ALL TABLES. So it feels
contradictory. Maybe it is just the comment that needs updating?

I thought more about this function and found that we can remove the
'except_flag' variable.

Since we can only use EXCEPT TABLE clause for ALL TABLES publication
and we cannot use FOR TABLE clause with ALL TABLES.
If for ALL TABLES publication we call this function, we will return an
except table list.
Else we will return a list of table to be included in publication.

I have added a comment to this behaviour.

~~~

5.
/*
* Gets list of relation oids for a publication that matches the except_flag.
*
* This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
* should use GetAllPublicationRelations().
*/
List *
GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt,
bool except_flag)
{
List *result;
Relation pubrelsrel;
ScanKeyData scankey;
SysScanDesc scan;
HeapTuple tup;

/* Find all publications associated with the relation. */
pubrelsrel = table_open(PublicationRelRelationId, AccessShareLock);

Existing bug? Isn't this a bogus comment?
/* Find all publications associated with the relation. */

Was that meant to be the other way around? -- e.g. Find all the
relations associated with the specified publication.

I think you are correct. I will create a separate thread for this change.

======
src/backend/commands/publicationcmds.c

6.
+ default:
+ /* shouldn't happen */
+ elog(ERROR, "invalid publication object type %d",
+ puballobj->pubobjtype);
+ break;

I think the ERROR is enough of a clue that it shouldn't happen. I felt
the comment was redundant.

There are multiple similar occurrences. See functions
'ObjectsInPublicationToOids', in publicationcmds.c.
There are some occurrences in the openssl.c file as well. But I also
think this comment is redundant.
I have removed the comment.

~~~

ObjectsInPublicationToOids:

7.
case PUBLICATIONOBJ_TABLE:
+ pubobj->pubtable->except = false;
+ *rels = lappend(*rels, pubobj->pubtable);
+ break;
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = true;
*rels = lappend(*rels, pubobj->pubtable);
break;
Those are very similar. How about combining like below?

case PUBLICATIONOBJ_TABLE:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = (pubobj->pubobjtype ==
PUBLICATIONOBJ_EXCEPT_TABLE);
*rels = lappend(*rels, pubobj->pubtable);
break;

Modified

~~

pub_contains_invalid_column:

8.
pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
bool pubviaroot, char pubgencols_type,
- bool *invalid_column_list,
+ bool puballtables, bool *invalid_column_list,
bool *invalid_gen_col)

The 'pub_via_root' and 'pubgencols_type' are parameters. Somehow it
seems more natural for the 'puballtables' to be passed before those,
because FOR ALL TABLES comes before WITH in the syntax.

Modified

~~~

CreatePublication:

9.
else if (!stmt->for_all_sequences)
- {
ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
&schemaidlist);

AFAICT, this function is refactored a lot because of the removal of
that '{'. It looks like mostly whitespace, but really, I think the
logic is quite different. I wasn't sure what that was about. Is it
related to this patch, or some other bugfix in passing or what?

This change is part of this patch.
This change is required to get a list of tables which are excluded
(for ALL TABLES publication).

I think the code related to schema should be inside the condition
'else if (!stmt->for_all_sequences)'
I have made the change for the same and also added a comment.

======
src/backend/commands/tablecmds.c

ATPrepChangePersistence:

10.
- GetRelationPublications(RelationGetRelid(rel)) != NIL)
+ list_length(GetRelationPublications(RelationGetRelid(rel), false)) > 0)

Isn't an empty List the same as a NIL list? Maybe that list_length()
change was not really needed.

Modified

======
src/backend/parser/gram.y

11.
drop_option_list pub_obj_list pub_all_obj_type_list
+ except_pub_obj_list opt_except_clause

Is this name consistent with the others? Should it be pub_except_obj_list?

I think pub_except_obj_list is consistent with others. Modified.

~~~

12.
%type <publicationobjectspec> PublicationObjSpec
+%type <publicationobjectspec> ExceptPublicationObjSpec
%type <publicationallobjectspec> PublicationAllObjSpec

Is this name consistent with the others? Should it be PublicationExceptObjSpec?

I agree. Modified.

~~~

CreatePublicationStmt:

13.
n->pubname = $3;
+ n->pubobjects = $5;

I noticed that sometimes there is a cast (List *) and other times
there is not. e.g. none here, but cast in AlterPublicationStmt. Why
the differences?

This change is not required in the latest patch. Due to discussion in [2]/messages/by-id/CAA4eK1KZ1Sb0soHp3HH2htwJ3=qka-eQjW35vOW3+4VeWw4VoQ@mail.gmail.com.

~~~

PublicationObjSpec:

14.
The comment for 'PublicationObjSpec' says "FOR TABLE and FOR TABLES IN
SCHEMA specifications". If that comment is correct, then why is this
patch changing this code? OTOH, if the code is correct, then does the
comment need updating?

We have only added "$$->location = @1;" for PublicationObjSpec.
I define the location of the '^' indicator while throwing an error. I
think we don't need to update comments for it?

======
src/bin/pg_dump/pg_dump.c

15.
Shouldn't there have already been some ALTER ... ADD ALL TABLE dump
code and test code implemented back in patch 0002?

This change is not required in the latest patch. Due to discussion in [2]/messages/by-id/CAA4eK1KZ1Sb0soHp3HH2htwJ3=qka-eQjW35vOW3+4VeWw4VoQ@mail.gmail.com.

~~~

dumpPublication:

16.
else if (pubinfo->puballtables)
+ {
+ SimplePtrListCell *cell;
+
appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+ /* Include exception tables if the publication has except tables */
+ for (cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == pubrinfo->publication)
+ {
+ tbinfo = pubrinfo->pubtable;
+
+ if (first)
+ {
+ appendPQExpBufferStr(query, " EXCEPT TABLE (");
+ first = false;
+ }
+ else
+ appendPQExpBufferStr(query, ", ");
+ appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+ }
+ }
+ if (!first)
+ appendPQExpBufferStr(query, ")");
+ }

16a.
SimplePtrListCell *cell can be declared as a for-loop variable.

~

16b.
The comment should say "EXCEPT TABLES" in uppercase.

~

16c.
I am not convinced you can use that 'first' flag like you are doing.
Isn't that interfering with the existing usage of that flag? Perhaps
another boolean just for this EXCEPT loop is needed.

~~~

Modified

getPublicationTables:

17.
+ if (strcmp(prexcept, "f") == 0)
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+ else
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+

...

+ if (strcmp(prexcept, "t") == 0)
+ simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+

Here you are comparing the same 'prexcept' flag for both "f" and "t".

I felt it was better if both comparisons are the same (e.g. both "t").

Or better still, assign a new boolean and avoid that 2nd strcmp
entirely -- e.g. except_flag = (strcmp(prexcept, "t") == 0);

Modified

======
src/bin/pg_dump/pg_dump_sort.c

DOTypeNameCompare:

18.
+ else if (obj1->objType == DO_PUBLICATION_EXCEPT_REL)
+ {
+ PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
+ PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
+
+ /* Sort by publication name, since (namespace, name) match the rel */
+ cmpval = strcmp(probj1->publication->dobj.name,
+ probj2->publication->dobj.name);
+ if (cmpval != 0)
+ return cmpval;
+ }

Isn't this identical to the previous code block? So can't you just add
DO_PUBLICATION_EXCEPT_REL to that condition?

Modified

======
src/bin/pg_dump/t/002_pg_dump.pl

19.
Missing test cases for ALTER? But also.

This change is not required in the latest patch. Due to discussion in [2]/messages/by-id/CAA4eK1KZ1Sb0soHp3HH2htwJ3=qka-eQjW35vOW3+4VeWw4VoQ@mail.gmail.com.

~~~

20.
Missing test cases for EXCEPT for INHERITED tables?

======
src/bin/psql/describe.c

describeOneTableDetails:

21.
I was wondering if the "describe" for tables (e.g. \d+) should also
show the publications where the table is an ECEPT TABLE? How else is
the user going to know it has been excluded by some publication?

I thought it would be sufficient to show only the list of
publications, the table is part of.
Users can check the excluded tables by checking the description of the
publication using \dRp+.
Will it be not sufficient?
I am not sure why we should show a list of publications which it is not part of?
Am I missing something thoughts?

======
src/bin/psql/tab-complete.in.c

ALTER PUBLICATION:

22.
The tab completion does not seem as good as it could be. e.g, there is
missing '(' and the for EXCEPT TABLE

~~~

Modified

CREATE PUBLICATION:

23.
The tab completion does not seem as good as it could be. e.g, there is
missing '(' and the for EXCEPT TABLE

Modified

======
src/test/regress/sql/publication.sql

24.
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1

As well as doing the "describes" for the publication, I think we need
to see the test cases for the describes of those excluded tables. e.g.
I imagine that they should also list the publications that they are
*excluded* from, right?

See Reply to comment 21.

~~~

25.
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);

25a.
Needs some explanatory comments here saying these are for testing the
EXCEPT with inherited tables (e.g. ONLY versus not).

~

25b.
I think you should be testing the '*' syntax here too.

~~~

I agree. Made the changes.

26.
+CREATE TABLE pub_sch1.tbl2 (a int);
SET client_min_messages = 'ERROR';
CREATE PUBLICATION testpub_reset FOR ALL TABLES, ALL SEQUENCES;
RESET client_min_messages;
@@ -1344,9 +1358,15 @@ ALTER PUBLICATION testpub_reset ADD ALL TABLES;

-- Can't add ALL TABLES to 'ALL TABLES' publication
ALTER PUBLICATION testpub_reset ADD ALL TABLES;
+ALTER PUBLICATION testpub_reset RESET;
+
+-- Verify adding EXCEPT TABLE
+ALTER PUBLICATION testpub_reset ADD ALL TABLES EXCEPT TABLE
(pub_sch1.tbl1, pub_sch1.tbl2);
+\dRp+ testpub_reset

DROP PUBLICATION testpub_reset;
DROP TABLE pub_sch1.tbl1;
+DROP TABLE pub_sch1.tbl2;
DROP SCHEMA pub_sch1;

It looks like that added CREATE TABLE (and RESET?) belongs more
appropriately within the scope of the new test "Verify adding EXCEPT
TABLE".

This change is not required in the latest patch. Due to discussion in [2]/messages/by-id/CAA4eK1KZ1Sb0soHp3HH2htwJ3=qka-eQjW35vOW3+4VeWw4VoQ@mail.gmail.com.

I have addressed the comments and attached the latest patch.
As per suggestion by Shveta and Amit, I have omitted the patches 0001,
0002, and 0004 (as per [2]/messages/by-id/CAA4eK1KZ1Sb0soHp3HH2htwJ3=qka-eQjW35vOW3+4VeWw4VoQ@mail.gmail.com). Will post these patches once 0003 patch
is RFC.

The new 0001 patch is to support EXCEPT TABLE for CREATE PUBLICATION
.. FOR ALL TABLES syntax. I have attached in [3]/messages/by-id/CANhcyEXwLrQsec6g+1dqWTKyJQMQMh=getj28C+zLL14BjuumA@mail.gmail.com.

[1]: /messages/by-id/CAA4eK1JEKs8qwwhRb1BCiMNduJ5ePUtFnTscrZt86UKWBkLxwg@mail.gmail.com
[2]: /messages/by-id/CAA4eK1KZ1Sb0soHp3HH2htwJ3=qka-eQjW35vOW3+4VeWw4VoQ@mail.gmail.com
[3]: /messages/by-id/CANhcyEXwLrQsec6g+1dqWTKyJQMQMh=getj28C+zLL14BjuumA@mail.gmail.com

Thanks,
Shlok Kyal

#157Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#146)
Re: Skipping schema changes in publication

On Fri, 21 Nov 2025 at 12:26, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Here are some review comments for your patch v28-0003 (EXCEPT TABLE ...).

The review of this patch is a WIP. In this post I only looked at the test code.

======
.../t/037_rep_changes_except_table.pl

1.
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for except table publications

Use uppercase: /except table/EXCEPT TABLE/

~~~

2.
There are lots of test cases dedicated to partiion-table testing. I
felt a bigger comment separating these major groups might be helpful.

Something like:

-- ============================================
-- EXCEPT TABLE test cases for normal tables
-- ============================================

and

-- ============================================
-- EXCEPT TABLE test cases for partition tables
-- ============================================

~~~

3.
+# Initialize publisher node
...
+# Create subscriber node

Those 2 comments should be almost alike -- e.g. both should say
"Initialize" or both should say "Create".

~~~

4.
+# Test replication with publications created using FOR ALL TABLES EXCEPT TABLE
+# clause.
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE SCHEMA sch1;
+ CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+ CREATE TABLE public.tab1(a int);
+));
+

That first sentence ("Test replication with ...") is not needed here.
The is just repeating the purpose of the entire file, so that comment
can replace the one at the top of this file.

~~~

5.
+# Insert some data and verify that inserted data is not replicated

Be explicit that we are referring to the excluded table.

SUGGESTION (e.g.)
Verify that data inserted to the excluded table is not replcated.

~~~

6.
+# Alter publication to exclude data changes in public.tab1 and verify that
+# subscriber does not get the changed data for this table.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ ALTER PUBLICATION tap_pub_schema RESET;
+ ALTER PUBLICATION tap_pub_schema ADD ALL TABLES EXCEPT TABLE
(sch1.tab1, public.tab1);
+ INSERT INTO public.tab1 VALUES(generate_series(1,10));
+));
+$node_publisher->wait_for_catchup('tap_sub_schema');
+

It is not strictly needed for these tests, but do you think it makes
more sense to also do an ALTER SUBSCRIPTION ... REFRESH PUBLICATION;
whenever you change the publications?

~~~

7.
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+

double-blank lines.

~~~

8.
I think it would be more helpful if the partition table test cases say
(in their comments) a lot more about the steps they are doing, and
what they expect the result to be. Sure, I can read all the code to
figure it out for each case, but it is better to know the test
intentions/expectations then verify they are doing the right thing.

~~~

9.
+ CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+ CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);

Maybe create this table to have *multiple* partitions. It might be
interesting later to see what happens when you try to EXCEPT only one
of the partitions.

I have addressed all the comments
Please find the updated patch in [1]/messages/by-id/CANhcyEXwLrQsec6g+1dqWTKyJQMQMh=getj28C+zLL14BjuumA@mail.gmail.com.

[1]: /messages/by-id/CANhcyEXwLrQsec6g+1dqWTKyJQMQMh=getj28C+zLL14BjuumA@mail.gmail.com

Thanks,
Shlok Kyal

#158shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#155)
Re: Skipping schema changes in publication

On Tue, Dec 9, 2025 at 11:17 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have removed the 0001 0002 and 0004 patches for now. Will post them
once 0003 patch is RFC.
Here is the update patch for "EXCEPT TABLE".

Thanks, I have not looked at new patch yet, but here are few comments
for v29-003:

1)
create_publication.sgml:
Please add one more example in the example section for EXCEPT using
'all tables, and all sequences' after the last existing one. This is
needed to show that ALL TABLES EXCEPT() and ALL SEQ are still possible
in single command.

2)
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>

We may add: This does not apply to a partitioned table, however.
(this will make it more clear similar to how existing doc has it for
'FOR TABLE' clause ). And then start details on partition.

3)
When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect

Can we simply say 'specifying a root partitioned table has no effect'.
This will make it consistent as the previous sentence also uses the
same term rather than 'non-leaf'.

4)
tab_root is a partitioned table with tab_part_1 and tab_part_2 as its
partitions.
In the first case, I receive a WARNING because the user excluded
tab_part_2 but its data will still be replicated through the root
table:

postgres=# create publication pub3 for all tables except (tab_part_2)
WITH (publish_via_partition_root=true);
WARNING: partition "tab_part_2" will be replicated as
publish_via_partition_root is "true"

But in the following case, no WARNING is shown:
postgres=# create publication pub4 for all tables except (tab_root)
WITH (publish_via_partition_root=false);
CREATE PUBLICATION

In this scenario, the user has excluded the root table, yet its data
will still be replicated because publish_via_partition_root = false.
Should we emit a warning in this case as well? Thoughts?

5)
publication_add_relation:
+ if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+ pub->pubviaroot)

Can we please bring both the 'pub' conditions together, as that seems
more understandable:
if (pub->alltables && pub->pubviaroot &&...)

6)
We have added pubid as argument to GetAllPublicationRelations to
exclude except-list tables.
We should change comment atop GetAllPublicationRelations() to indicate
the same. We should extend
this existing comment to say about except-list exclusion also.

* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables.

thanks
Shveta

#159Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#158)
1 attachment(s)
Re: Skipping schema changes in publication

On Wed, 10 Dec 2025 at 11:21, shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Dec 9, 2025 at 11:17 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have removed the 0001 0002 and 0004 patches for now. Will post them
once 0003 patch is RFC.
Here is the update patch for "EXCEPT TABLE".

Thanks, I have not looked at new patch yet, but here are few comments
for v29-003:

1)
create_publication.sgml:
Please add one more example in the example section for EXCEPT using
'all tables, and all sequences' after the last existing one. This is
needed to show that ALL TABLES EXCEPT() and ALL SEQ are still possible
in single command.

2)
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+     </para>

We may add: This does not apply to a partitioned table, however.
(this will make it more clear similar to how existing doc has it for
'FOR TABLE' clause ). And then start details on partition.

3)
When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a partitioned table or non-leaf
+      partition has no effect

Can we simply say 'specifying a root partitioned table has no effect'.
This will make it consistent as the previous sentence also uses the
same term rather than 'non-leaf'.

4)
tab_root is a partitioned table with tab_part_1 and tab_part_2 as its
partitions.
In the first case, I receive a WARNING because the user excluded
tab_part_2 but its data will still be replicated through the root
table:

postgres=# create publication pub3 for all tables except (tab_part_2)
WITH (publish_via_partition_root=true);
WARNING: partition "tab_part_2" will be replicated as
publish_via_partition_root is "true"

But in the following case, no WARNING is shown:
postgres=# create publication pub4 for all tables except (tab_root)
WITH (publish_via_partition_root=false);
CREATE PUBLICATION

In this scenario, the user has excluded the root table, yet its data
will still be replicated because publish_via_partition_root = false.
Should we emit a warning in this case as well? Thoughts?

5)
publication_add_relation:
+ if (pub->alltables && pri->except && targetrel->rd_rel->relispartition &&
+ pub->pubviaroot)

Can we please bring both the 'pub' conditions together, as that seems
more understandable:
if (pub->alltables && pub->pubviaroot &&...)

6)
We have added pubid as argument to GetAllPublicationRelations to
exclude except-list tables.
We should change comment atop GetAllPublicationRelations() to indicate
the same. We should extend
this existing comment to say about except-list exclusion also.

* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables.

Hi Shveta,

I have addressed the above comments and attached the updated patch.
I have also addressed a comment by Peter (comment no. 20 in [1]/messages/by-id/CAHut+Pudi+9ssBR_Q_Fd29aGEu8s18OyKUGo5w5aKJK-2_c+8g@mail.gmail.com) which
I missed in the earlier version.

[1]: /messages/by-id/CAHut+Pudi+9ssBR_Q_Fd29aGEu8s18OyKUGo5w5aKJK-2_c+8g@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v31-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v31-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 641d9200052630e425f925b0a65d27e601836640 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Dec 2025 22:41:23 +0530
Subject: [PATCH v31] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |   9 +
 doc/src/sgml/logical-replication.sgml         |  10 +-
 doc/src/sgml/ref/create_publication.sgml      |  56 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          | 135 ++++++++---
 src/backend/commands/publicationcmds.c        | 105 ++++++---
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  34 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  27 +--
 src/backend/utils/cache/relcache.c            |  24 +-
 src/bin/pg_dump/pg_dump.c                     |  55 ++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/pg_dump_sort.c                |  10 +-
 src/bin/pg_dump/t/002_pg_dump.pl              |  32 ++-
 src/bin/psql/describe.c                       |  58 ++++-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_publication.h          |   7 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   5 +
 src/test/regress/expected/publication.out     |  57 ++++-
 src/test/regress/sql/publication.sql          |  21 +-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 215 ++++++++++++++++++
 24 files changed, 768 insertions(+), 123 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..a4d32de58ec 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation must be excluded
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index aa013f348d4..c420469feaa 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2550,10 +2550,12 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..528660e011a 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">table_exception_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>where <replaceable class="parameter">table_exception_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. If
+      <literal>EXCEPT TABLE</literal> is specified, then exclude replicating
+      the changes for the specified tables.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +190,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. It can only be used with <literal>FOR ALL TABLES</literal>.
+      If <literal>ONLY</literal> is specified before the table name, only
+      that table is excluded from the publication. If <literal>ONLY</literal> is
+      not specified, the table and all its descendant tables (if any) are
+      excluded. Optionally, <literal>*</literal> can be specified after the
+      table name to explicitly indicate that descendant tables are excluded.
+      This does not apply to a partitioned table, however.
+     </para>
+     <para>
+      The partitioned table or its partitions are excluded from the publication
+      based on the parameter <literal>publish_via_partition_root</literal>.
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a root partitioned table has no
+      effect, as changes are replicated via the leaf partitions. Specifying a
+      leaf partition excludes only that partition from replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -487,6 +522,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES;
    all sequences for synchronization:
 <programlisting>
 CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all the tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all sequences and all the
+   tables except tables <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT (users, departments);
 </programlisting>
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index be5ef5e4c0e..4742927cdb6 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,43 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = NIL;
+		List	   *aexceptpubids = NIL;
+		List	   *aschemapubids = NIL;
+		bool		set_top = false;
+
+		GetRelationPublications(ancestor, &apubids, &aexceptpubids);
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+				set_top = !list_member_oid(aexceptpubids, puboid);
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aschemapubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +478,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Check when a partition is excluded via EXCEPT TABLE while the
+	 * publication has publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relispartition)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
+	/*
+	 * Check when a partitioned table is excluded via EXCEPT TABLE while the
+	 * publication has publish_via_partition_root = false.
+	 */
+	if (pub->alltables && !pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		ereport(WARNING,
+				(errmsg("partitioned table \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "false")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +514,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,35 +783,59 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
-List *
-GetRelationPublications(Oid relid)
+/*
+ * Get the list of publication oids associated with a specified relation.
+ * pubids is filled with the list of publication oids the relation is part of.
+ * except_pubids is filled with the list of publication oids the relation is
+ * excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */
+bool
+GetRelationPublications(Oid relid, List **pubids, List **except_pubids)
 {
-	List	   *result = NIL;
 	CatCList   *pubrellist;
-	int			i;
+	bool		found = false;
 
 	/* Find all publications associated with the relation. */
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
-		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		Oid			pubid = pubrel->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (pubrel->prexcept)
+		{
+			if (except_pubids)
+				*except_pubids = lappend_oid(*except_pubids, pubid);
+		}
+		else
+		{
+			if (pubids)
+				*pubids = lappend_oid(*pubids, pubid);
+			found = true;
+		}
 	}
 
 	ReleaseSysCacheList(pubrellist);
 
-	return result;
+	return found;
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR ALL TABLES publication, this returns the list of tables that were
+ * explicitly excluded via an EXCEPT TABLE clause.
+ *
+ * For a FOR TABLE publication, this returns the list of tables explicitly
+ * included in the publication.
  *
- * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
  */
 List *
 GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
@@ -864,15 +922,23 @@ GetAllTablesPublications(void)
  * partitioned tables, we must exclude partitions in favor of including the
  * root partitioned tables. This is not applicable to FOR ALL SEQUENCES
  * publication.
+ *
+ * The list does not include relations that are explicitly excluded via the
+ * EXCEPT TABLE clause of the publication specified by pubid.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist;
+
+	exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+										 PUBLICATION_PART_ALL :
+										 PUBLICATION_PART_ROOT);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +957,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +979,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,7 +1236,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
@@ -1367,7 +1436,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a1983508950..790f9e6948e 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,38 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +226,8 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = (pubobj->pubobjtype == PUBLICATIONOBJ_EXCEPT_TABLE);
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +302,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool pubviaroot, bool puballtables)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +329,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -354,8 +389,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool pubviaroot,
+							char pubgencols_type, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +414,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -923,16 +959,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
 	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
@@ -944,22 +973,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
-
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
-
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
-
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
-
 		if (schemaidlist != NIL)
 		{
 			/*
@@ -971,8 +984,37 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 		}
 	}
 
+	/*
+	 * If publication is for ALL TABLES and relations is not empty, it means
+	 * that there are some relations to be excluded from the publication.
+	 * Else, relations is the list of relations to be added to the
+	 * publication.
+	 */
+	if (relations != NIL)
+	{
+		List	   *rels;
+
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
+
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
+
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
+
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1348,6 +1390,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc_object(PublicationRelInfo);
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1761,6 +1804,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1833,6 +1877,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1c9ef53be20..85080d5aced 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8651,7 +8651,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18846,7 +18846,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7856ce9d78f..c523512a097 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -454,6 +454,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				pub_except_obj_list opt_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -591,6 +592,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 <publicationobjectspec> PublicationExceptObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10761,6 +10763,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10801,6 +10804,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10877,10 +10881,13 @@ pub_obj_list:	PublicationObjSpec
 	;
 
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10897,6 +10904,31 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+/*
+ * ALL TABLES EXCEPT ( table_name [, ...] ) specification
+ */
+PublicationExceptObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+pub_except_obj_list: PublicationExceptObjSpec
+					{ $$ = list_make1($1); }
+			| pub_except_obj_list ',' PublicationExceptObjSpec
+					{ $$ = lappend($1, $3); }
+	;
+
+opt_except_clause:
+			EXCEPT opt_table '(' pub_except_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
 
 /*****************************************************************************
  *
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..d042da7b347 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = NIL;
+		List	   *exceptTablePubids = NIL;
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2099,6 +2100,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		char		relkind = get_rel_relkind(relid);
 		List	   *rel_publications = NIL;
 
+		GetRelationPublications(relid, &pubids, &exceptTablePubids);
+
 		/* Reload publications if needed before use. */
 		if (!publications_valid)
 		{
@@ -2195,22 +2198,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2216,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2232,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2312,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index a4dc1cbe5ae..f554a31a27b 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5793,7 +5793,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
-	List	   *puboids;
+	List	   *puboids = NIL;
+	List	   *exceptpuboids = NIL;
+	List	   *alltablespuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	GetRelationPublications(relid, &puboids, &exceptpuboids);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5843,16 +5845,25 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		foreach(lc, ancestors)
 		{
 			Oid			ancestor = lfirst_oid(lc);
+			List	   *ancestor_puboids = NIL;
+			List	   *ancestor_exceptpuboids = NIL;
 
-			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+			GetRelationPublications(ancestor, &ancestor_puboids,
+									&ancestor_exceptpuboids);
+
+			puboids = list_concat_unique_oid(puboids, ancestor_puboids);
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   ancestor_exceptpuboids);
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,7 +5894,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
-										   pubform->pubviaroot))
+										   pubform->pubviaroot, pubform->puballtables))
 		{
 			if (pubform->pubupdate)
 				pubdesc->rf_valid_for_update = false;
@@ -5899,6 +5910,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		 */
 		if ((pubform->pubupdate || pubform->pubdelete) &&
 			pub_contains_invalid_column(pubid, relation, ancestors,
+										pubform->puballtables,
 										pubform->pubviaroot,
 										pubform->pubgencols,
 										&invalid_column_list,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ad201af2f..b04f4d72494 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -186,6 +186,8 @@ static SimpleOidList extension_include_oids = {NULL, NULL};
 static SimpleStringList extension_exclude_patterns = {NULL, NULL};
 static SimpleOidList extension_exclude_oids = {NULL, NULL};
 
+static SimplePtrList exceptinfo = {NULL, NULL};
+
 static const CatalogId nilCatalogId = {0, 0};
 
 /* override for standard extra_float_digits setting */
@@ -4676,7 +4678,33 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		bool first_tbl = true;
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has EXCEPT TABLEs */
+		for (SimplePtrListCell *cell = exceptinfo.head; cell; cell = cell->next)
+		{
+			PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+			TableInfo  *tbinfo;
+
+			if (pubinfo == pubrinfo->publication)
+			{
+				tbinfo = pubrinfo->pubtable;
+
+				if (first_tbl)
+				{
+					appendPQExpBufferStr(query, " EXCEPT TABLE (");
+					first_tbl = false;
+				}
+				else
+					appendPQExpBufferStr(query, ", ");
+				appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+			}
+		}
+		if (!first_tbl)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4845,6 +4873,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_prrelid;
 	int			i_prrelqual;
 	int			i_prattrs;
+	int			i_prexcept;
 	int			i,
 				j,
 				ntups;
@@ -4856,8 +4885,16 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
+		appendPQExpBufferStr(query,
+							 "SELECT tableoid, oid, prpubid, prrelid,\n");
+
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " prexcept,\n");
+		else
+			appendPQExpBufferStr(query, " false AS prexcept,\n");
+
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4868,6 +4905,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -4883,6 +4921,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_prrelid = PQfnumber(res, "prrelid");
 	i_prrelqual = PQfnumber(res, "prrelqual");
 	i_prattrs = PQfnumber(res, "prattrs");
+	i_prexcept = PQfnumber(res, "prexcept");
 
 	/* this allocation may be more than we need */
 	pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo));
@@ -4894,6 +4933,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			prrelid = atooid(PQgetvalue(res, i, i_prrelid));
 		PublicationInfo *pubinfo;
 		TableInfo  *tbinfo;
+		bool		prexcept = strcmp(PQgetvalue(res, i, i_prexcept), "t") == 0;
 
 		/*
 		 * Ignore any entries for which we aren't interested in either the
@@ -4907,7 +4947,11 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 			continue;
 
 		/* OK, make a DumpableObject for this relationship */
-		pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+		if (prexcept)
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+		else
+			pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+
 		pubrinfo[j].dobj.catId.tableoid =
 			atooid(PQgetvalue(res, i, i_tableoid));
 		pubrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
@@ -4948,6 +4992,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 		/* Decide whether we want to dump it */
 		selectDumpablePublicationObject(&(pubrinfo[j].dobj), fout);
 
+		if (prexcept)
+			simple_ptr_list_append(&exceptinfo, &pubrinfo[j]);
+
 		j++;
 	}
 
@@ -11826,6 +11873,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20196,6 +20246,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..723b5575c53 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..6ebeb9c96a1 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -432,7 +434,8 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
-	else if (obj1->objType == DO_PUBLICATION_REL)
+	else if (obj1->objType == DO_PUBLICATION_REL ||
+			 obj1->objType == DO_PUBLICATION_EXCEPT_REL)
 	{
 		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
 		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
@@ -1715,6 +1718,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..db18a40744c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,36 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub10' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table_generated);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table_generated, ONLY dump_test.test_table_generated_child2, ONLY dump_test.test_table_generated_child1) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
@@ -5163,7 +5193,7 @@ foreach my $run (sort keys %pgdump_runs)
 		#
 		# Either "all_runs" should be set or there should be a "like" list,
 		# even if it is empty.  (This makes the test more self-documenting.)
-		if (!defined($tests{$test}->{all_runs})
+		if (   !defined($tests{$test}->{all_runs})
 			&& !defined($tests{$test}->{like}))
 		{
 			die "missing \"like\" in test \"$test\"";
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..50b1d435359 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -6753,8 +6770,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6793,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 20d7a65c614..45e7a9cbfd3 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3623,7 +3623,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..c3a5e278a03 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,11 +146,12 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern bool GetRelationPublications(Oid relid, List **pubids, List **except_pubids);
 
 /*---------
  * Expected values for pub_partopt parameter of GetPublicationRelations(),
@@ -170,7 +171,7 @@ typedef enum PublicationPartOpt
 
 extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -181,7 +182,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..4a170994f76 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
-										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										List *ancestors, bool puballtables,
+										bool pubviaroot, char pubgencols_type,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..a14ecedb27f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4271,6 +4271,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4279,6 +4280,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4307,6 +4309,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
@@ -4326,6 +4329,7 @@ typedef enum AlterPublicationAction
 	AP_AddObjects,				/* add objects to publication */
 	AP_DropObjects,				/* remove objects from publication */
 	AP_SetObjects,				/* set list of objects */
+	AP_Reset,					/* reset the publication */
 } AlterPublicationAction;
 
 typedef struct AlterPublicationStmt
@@ -4341,6 +4345,7 @@ typedef struct AlterPublicationStmt
 	 * objects.
 	 */
 	List	   *pubobjects;		/* Optional list of publication objects */
+	bool		for_all_tables; /* Special publication for all tables in db */
 	AlterPublicationAction action;	/* What action to perform with the given
 									 * objects */
 } AlterPublicationStmt;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..ef469c761d0 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,40 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+-- EXCEPT with wildcard: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3*);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -238,8 +265,34 @@ Tables:
 Tables:
     "public.testpub_tbl3"
 
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+    "public.testpub_tbl3a"
+
+\dRp+ testpub7
+                                                      Publication testpub7
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl3"
+
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..651b3b12030 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,37 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
 CREATE TABLE testpub_tbl3 (a int);
 CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);
+-- EXCEPT with wildcard: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3*);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl3);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
+\dRp+ testpub7
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..09174b7d5d7
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,215 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT TABLE publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# ============================================
+# EXCEPT TABLE test cases for normal tables
+# ============================================
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Verify that data inserted to the excluded table is not replicated.
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+# ============================================
+# EXCEPT TABLE test cases for partition tables
+# ============================================
+# Check behavior of EXCEPT TABLE together with publish_via_partition_root
+# when applied to a partitioned table and its partitions.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+	CREATE TABLE sch1.part2(a int);
+));
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = false
+# Excluding a partition while publish_via_partition_root = false prevents
+# replication of rows inserted into the partitioned table.
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)"
+);
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on other partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table while publish_via_partition_root = false
+# still allows rows inserted into its partitions to be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = true
+# When the partitioned table is excluded and publish_via_partition_root is true,
+# no rows from the table or its partitions are replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = true
+# When a partition is excluded but publish_via_partition_root is true,
+# rows published through the partitioned table can still be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1 ORDER BY a");
+is( $result, qq(1
+2
+6
+7), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on other partition');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#160Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#159)
Re: Skipping schema changes in publication

Hi Shlok -

Here are some review comments for v31-0001 (EXCEPT (tablelist))

======
Commit message

1.
The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

~

In v30, you removed all the ALTER PUBLICATION changes, so the "or
altering" in the message above also needs to be removed.

======
doc/src/sgml/logical-replication.sgml

2.
   <para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a
superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
   </para>

This is a good improvement, but I was not sure why it is in this
patch. Should it be a separate thread for a docs improvement?

======
src/backend/catalog/pg_publication.c

GetTopMostAncestorInPublication:

3.
{
  Oid ancestor = lfirst_oid(lc);
- List    *apubids = GetRelationPublications(ancestor);
- List    *aschemaPubids = NIL;
+ List    *apubids = NIL;
+ List    *aexceptpubids = NIL;
+ List    *aschemapubids = NIL;
+ bool set_top = false;
+
+ GetRelationPublications(ancestor, &apubids, &aexceptpubids);

level++;

- if (list_member_oid(apubids, puboid))
+ /* check if member of table publications */
+ set_top = list_member_oid(apubids, puboid);
+ if (!set_top)
  {
- topmost_relid = ancestor;
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (ancestor_level)
- *ancestor_level = level;
+ /* check if member of schema publications */
+ set_top = list_member_oid(aschemapubids, puboid);
+
+ /*
+ * If the publication is all tables publication and the table is
+ * not part of exception tables.
+ */
+ if (!set_top && puballtables)
+ set_top = !list_member_oid(aexceptpubids, puboid);
  }
- else
+
+ if (set_top)
  {
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
- {
- topmost_relid = ancestor;
+ topmost_relid = ancestor;
- if (ancestor_level)
- *ancestor_level = level;
- }
+ if (ancestor_level)
+ *ancestor_level = level;
  }
  list_free(apubids);
- list_free(aschemaPubids);
+ list_free(aschemapubids);
+ list_free(aexceptpubids);
  }

That 'aschemapubids' can be declared and freed within the if block.

~~~

publication_add_relation:

4.
+ /*
+ * Check when a partition is excluded via EXCEPT TABLE while the
+ * publication has publish_via_partition_root = true.
+ */
+ if (pub->alltables && pub->pubviaroot && pri->except &&
+ targetrel->rd_rel->relispartition)
+ ereport(WARNING,

This comment doesn't sound quite right:

SUGGESTION
Handle the case where a partition is excluded by EXCEPT TABLE while
publish_via_partition_root = true.

~~~

5.
+ /*
+ * Check when a partitioned table is excluded via EXCEPT TABLE while the
+ * publication has publish_via_partition_root = false.
+ */
+ if (pub->alltables && !pub->pubviaroot && pri->except &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(WARNING,

Ditto. Reword like suggested in the previous review comment.

~~~

6.
+/*
+ * Get the list of publication oids associated with a specified relation.
+ * pubids is filled with the list of publication oids the relation is part of.
+ * except_pubids is filled with the list of publication oids the relation is
+ * excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */

Maybe putting 'pubids' and 'except_pubids' in single quotes will help
readability of this comment?

Also, these are already Lists, so they are not filled with lists.

SUGGESTION
Parameter 'pubids' returns the OIDs of the publications the relation is part of.
Parameter 'except_pubids' returns the OIDs of publications the
relation is excluded from.

~~~

GetPublicationRelations:

7.
 /*
- * Gets list of relation oids for a publication.
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR ALL TABLES publication, this returns the list of tables that were
+ * explicitly excluded via an EXCEPT TABLE clause.
+ *
+ * For a FOR TABLE publication, this returns the list of tables explicitly
+ * included in the publication.
  *
- * This should only be used FOR TABLE publications, the FOR ALL
TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
  */
 List *
 GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)

7a.
The function is called 'GetPublicationRelations', so it seems
unintuitive that it sometimes returns the list of all the tables that
are *excluded* from the publication. If you are going to have one
single function that does everything, then IMO it might be better to
hide that behind some wrapper functions like:
GetPublicationMemberRelations
GetPublicationExcludedRelations

Consider also that all these assumptions might be OK today but they
won't be OK in the future. e.g. One day, when named FOR SEQUENCE
sq1,sq2 are supported then you will be alble to write a command like
FOR ALL TABLES EXCEPT (t1), FOR SEQUENCE sq1,sq2. That's going to be a
muddle of some included and some excluded relations. So, it is better
to cater for that scenario now, rather than have to rewrite all of
this function again in the future. e.g. Maybe instead of this function
returning one list it is better to return included/excluded Lists or
relations as output parameters?

~

7b.
Also, comments like "Publications declared with FOR ALL TABLES or FOR
ALL SEQUENCES should use..." seems like too many assumptions are being
made. It would be better to enforce the calling requirements using
parameter checking and Asserts instead instead of hoping that callers
are going to abide by the comments.

~~~

GetAllPublicationRelations:

8.
+ exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+ PUBLICATION_PART_ALL :
+ PUBLICATION_PART_ROOT);

This is similar to the above review comment. I'm not sure how you can
just assume that this must be the "except list" -- AFAICT this assumes
that 'GetAllPublicationRelations' can only be called by FOR ALL TABLES
(???). Seems like a lot of assumptions, that would be much better to
be enforced by Asserts in the code.

======
src/backend/commands/publicationcmds.c

pub_rf_contains_invalid_column:

9.
bool
pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
- bool pubviaroot)
+ bool pubviaroot, bool puballtables)

I felt that 'puballtables' is more "important" than 'pubviaroot' so
maybe it should come earlier in the parameter list. (e.g. make it more
similar to 'pub_contains_invalid_column')

======
src/backend/parser/gram.y

10.
+ pub_except_obj_list opt_except_clause

I felt that 'opt_except_clause' should better be called
'opt_pub_except_clause' or 'pub_opt_except_clause' because without
'pub' it is a bit vague.

~~~

11.
+/*
+ * ALL TABLES EXCEPT ( table_name [, ...] ) specification
+ */

11a
This comment should be up where all the other CREATE PUBLICATION
syntax is commented.

~

11b.
Also, there is a missing optional "[TABLE]" part.

~~~

12.
+pub_except_obj_list: PublicationExceptObjSpec
+ { $$ = list_make1($1); }
+ | pub_except_obj_list ',' PublicationExceptObjSpec
+ { $$ = lappend($1, $3); }
+ ;
+
+opt_except_clause:
+ EXCEPT opt_table '(' pub_except_obj_list ')' { $$ = $4; }
+ | /*EMPTY*/ { $$ = NIL; }
+ ;

I felt the clause should be defined before the obj list because that
seems the natural order to read these.

======
src/bin/pg_dump/pg_dump.c

13.
+static SimplePtrList exceptinfo = {NULL, NULL};

Having this as global seems a bit hacky. It has nothing in common with
all the other nearby lists, which are commented as being based on
"patterns given by command-line switches"

~~~

dumpPublication:

14.
+ /* Include exception tables if the publication has EXCEPT TABLEs */
+ for (SimplePtrListCell *cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == pubrinfo->publication)
+ {
+ tbinfo = pubrinfo->pubtable;

That 'tbinfo' can be declared within the "if".

~~~

15.
+ appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));

ONLY is not the default. How did you decide that "ONLY" is the correct
thing to do here?

~~~

getPublicationTables:

16.
- pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+ if (prexcept)
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+ else
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+

Would a single assignment (ternary) make this code simpler and easier to read?

SUGGESTION
pubrinfo[j].dobj.objType = prexcept ?
DO_PUBLICATION_EXCEPT_REL :
DO_PUBLICATION_REL;

======
src/bin/pg_dump/t/002_pg_dump.pl

17.
+ 'CREATE PUBLICATION pub10' => {
+ create_order => 50,
+ create_sql =>
+   'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE
(dump_test.test_table_generated);',
+ regexp => qr/^
+ \QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY
dump_test.test_table_generated, ONLY
dump_test.test_table_generated_child2, ONLY
dump_test.test_table_generated_child1) WITH (publish = 'insert,
update, delete, truncate');\E
+ /xm,
+ like => { %full_runs, section_post_data => 1, },
+ },
+

These "generated" names seem unusual. I saw there are some other
tables like 'dump_test.test_inheritance_child' and
'dump_test.test_inheritance_parent'. Can you use those more normal
table names instead?

Also curious - does the order of the tests matter? I saw that the
CREATE TABLE tests seem to be coming after the CREATE PUBLICATION
tests that are using them.

~~~
18.
- if (!defined($tests{$test}->{all_runs})
+ if (   !defined($tests{$test}->{all_runs})

Why add this whitespace?

======
src/include/nodes/parsenodes.h

19.
AP_SetObjects, /* set list of objects */
+ AP_Reset, /* reset the publication */
} AlterPublicationAction;

AFAIK, you removed all ALTER command changes from v30-0001. So this
should not be here.

~~~

20.
+ bool for_all_tables; /* Special publication for all tables in db */
AlterPublicationAction action; /* What action to perform with the given
* objects */
} AlterPublicationStmt;

AFAIK, you removed all ALTER command changes from v30-0001. So this
should not be here.

======
src/test/regress/sql/publication.sql

21.
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES
EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES
EXCEPT (testpub_tbl1);

Should be 2 comments here for the 2x CREATE:

# Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)

# Exclude tables using FOR ALL TABLES EXCEPT (tablelist)

~~~

22.
CREATE TABLE testpub_tbl3 (a int);
CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);

If you rename these tables like 'testpub_tbl_parent' and
'testpub_tbl_child', it will be much easier to see what is going on.

~~~

23.
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);

Missing comment -- something like:
# Exclude parent table, omitting both of 'ONLY' and '*'

~~~

24.
+-- EXCEPT with wildcard: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3*);

24a.
TBH, I don't think this is a "wildcard" -- it is not doing any pattern
matching. IMO just call it an "asterisk" or a "star".

~

24b.
And put a space before the '*' here.

======
.../t/037_rep_changes_except_table.pl

25.
+# ============================================
+# EXCEPT TABLE test cases for partition tables
+# ============================================
+# Check behavior of EXCEPT TABLE together with publish_via_partition_root
+# when applied to a partitioned table and its partitions.

Really, that "Check behavior" sentence is generic for all of the
following tests, so it should also be (within the "=======" of the
previous comment)

~~~

26.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+ CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+ INSERT INTO sch1.t1 VALUES (1), (6);
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE sch1.t1(a int);
+ CREATE TABLE sch1.part1(a int);
+ CREATE TABLE sch1.part2(a int);
+));

26a.
There should be a comment for this part that just says something like
"Setup partition table and partitions on the publisher that map to
normal tables on the subscriber"

~

26b.
The INSERT should be done later, after the CREATE PUBLICATION but
before the CREATE SUBSCRIPTION. The pattern will be the same for all
the test cases.

~~~

27.
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)"
+);

Even though the publish_via_partition_root is 'false' by default, I
think you should spell it out explicitly here for clarity.

~~~

28.
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table while publish_via_partition_root = false
+# still allows rows inserted into its partitions to be replicated.

I felt you should word this differently. I don't think you should say
"inserted into its partitions" because actually, you inserted into the
partition table, and the data just ends up in the partitions.

~~~

29.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1);
+ INSERT INTO sch1.t1 VALUES (1), (6);
+));

Ditto earlier comment. Better to explicitly say
"publish_via_partition_root=false".

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

#161Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#156)
Re: Skipping schema changes in publication

On Wed, Dec 10, 2025 at 4:49 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 24 Nov 2025 at 13:03, Peter Smith <smithpb2250@gmail.com> wrote:

...

21.
I was wondering if the "describe" for tables (e.g. \d+) should also
show the publications where the table is an ECEPT TABLE? How else is
the user going to know it has been excluded by some publication?

I thought it would be sufficient to show only the list of
publications, the table is part of.
Users can check the excluded tables by checking the description of the
publication using \dRp+.
Will it be not sufficient?
I am not sure why we should show a list of publications which it is not part of?
Am I missing something thoughts?

For this comment, I was imagining a scenario where there are dozens of
publications, and the user is wondering why their table is not being
replicated to the subscriber like they expected it would be.

Yes, they could use \dRs+ to identify the publications excluding it,
but that will be quite painful if there are very many publications
they have to check. IIUC, there is no other way to check it without
digging into System Catalogs.

That's why I thought it might be useful if the \d+ could also show
publications where the table was named in an EXCEPT TABLE clause.

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

#162Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#160)
1 attachment(s)
Re: Skipping schema changes in publication

On Thu, 11 Dec 2025 at 04:10, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok -

Here are some review comments for v31-0001 (EXCEPT (tablelist))

======
Commit message

1.
The new syntax allows specifying excluded relations when creating or altering
a publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

~

In v30, you removed all the ALTER PUBLICATION changes, so the "or
altering" in the message above also needs to be removed.

======
doc/src/sgml/logical-replication.sgml

2.
<para>
-   To add tables to a publication, the user must have ownership rights on the
-   table. To add all tables in schema to a publication, the user must be a
-   superuser. To create a publication that publishes all tables, all tables in
-   schema, or all sequences automatically, the user must be a superuser.
+   To create a publication using <literal>FOR ALL TABLES</literal>,
+   <literal>FOR ALL SEQUENCES</literal> or
+   <literal>FOR TABLES IN SCHEMA</literal>, the user must be a
superuser. To add
+   <literal>ALL TABLES</literal> or <literal>TABLES IN SCHEMA</literal> to a
+   publication, the user must be a superuser. To add tables to a publication,
+   the user must have ownership rights on the table.
</para>

This is a good improvement, but I was not sure why it is in this
patch. Should it be a separate thread for a docs improvement?

======
src/backend/catalog/pg_publication.c

GetTopMostAncestorInPublication:

3.
{
Oid ancestor = lfirst_oid(lc);
- List    *apubids = GetRelationPublications(ancestor);
- List    *aschemaPubids = NIL;
+ List    *apubids = NIL;
+ List    *aexceptpubids = NIL;
+ List    *aschemapubids = NIL;
+ bool set_top = false;
+
+ GetRelationPublications(ancestor, &apubids, &aexceptpubids);

level++;

- if (list_member_oid(apubids, puboid))
+ /* check if member of table publications */
+ set_top = list_member_oid(apubids, puboid);
+ if (!set_top)
{
- topmost_relid = ancestor;
+ aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (ancestor_level)
- *ancestor_level = level;
+ /* check if member of schema publications */
+ set_top = list_member_oid(aschemapubids, puboid);
+
+ /*
+ * If the publication is all tables publication and the table is
+ * not part of exception tables.
+ */
+ if (!set_top && puballtables)
+ set_top = !list_member_oid(aexceptpubids, puboid);
}
- else
+
+ if (set_top)
{
- aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
- if (list_member_oid(aschemaPubids, puboid))
- {
- topmost_relid = ancestor;
+ topmost_relid = ancestor;
- if (ancestor_level)
- *ancestor_level = level;
- }
+ if (ancestor_level)
+ *ancestor_level = level;
}
list_free(apubids);
- list_free(aschemaPubids);
+ list_free(aschemapubids);
+ list_free(aexceptpubids);
}

That 'aschemapubids' can be declared and freed within the if block.

~~~

publication_add_relation:

4.
+ /*
+ * Check when a partition is excluded via EXCEPT TABLE while the
+ * publication has publish_via_partition_root = true.
+ */
+ if (pub->alltables && pub->pubviaroot && pri->except &&
+ targetrel->rd_rel->relispartition)
+ ereport(WARNING,

This comment doesn't sound quite right:

SUGGESTION
Handle the case where a partition is excluded by EXCEPT TABLE while
publish_via_partition_root = true.

~~~

5.
+ /*
+ * Check when a partitioned table is excluded via EXCEPT TABLE while the
+ * publication has publish_via_partition_root = false.
+ */
+ if (pub->alltables && !pub->pubviaroot && pri->except &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(WARNING,

Ditto. Reword like suggested in the previous review comment.

~~~

6.
+/*
+ * Get the list of publication oids associated with a specified relation.
+ * pubids is filled with the list of publication oids the relation is part of.
+ * except_pubids is filled with the list of publication oids the relation is
+ * excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */

Maybe putting 'pubids' and 'except_pubids' in single quotes will help
readability of this comment?

Also, these are already Lists, so they are not filled with lists.

SUGGESTION
Parameter 'pubids' returns the OIDs of the publications the relation is part of.
Parameter 'except_pubids' returns the OIDs of publications the
relation is excluded from.

~~~

GetPublicationRelations:

7.
/*
- * Gets list of relation oids for a publication.
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR ALL TABLES publication, this returns the list of tables that were
+ * explicitly excluded via an EXCEPT TABLE clause.
+ *
+ * For a FOR TABLE publication, this returns the list of tables explicitly
+ * included in the publication.
*
- * This should only be used FOR TABLE publications, the FOR ALL
TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
*/
List *
GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)

7a.
The function is called 'GetPublicationRelations', so it seems
unintuitive that it sometimes returns the list of all the tables that
are *excluded* from the publication. If you are going to have one
single function that does everything, then IMO it might be better to
hide that behind some wrapper functions like:
GetPublicationMemberRelations
GetPublicationExcludedRelations

Consider also that all these assumptions might be OK today but they
won't be OK in the future. e.g. One day, when named FOR SEQUENCE
sq1,sq2 are supported then you will be alble to write a command like
FOR ALL TABLES EXCEPT (t1), FOR SEQUENCE sq1,sq2. That's going to be a
muddle of some included and some excluded relations. So, it is better
to cater for that scenario now, rather than have to rewrite all of
this function again in the future. e.g. Maybe instead of this function
returning one list it is better to return included/excluded Lists or
relations as output parameters?

~

7b.
Also, comments like "Publications declared with FOR ALL TABLES or FOR
ALL SEQUENCES should use..." seems like too many assumptions are being
made. It would be better to enforce the calling requirements using
parameter checking and Asserts instead instead of hoping that callers
are going to abide by the comments.

~~~

GetAllPublicationRelations:

8.
+ exceptlist = GetPublicationRelations(pubid, pubviaroot ?
+ PUBLICATION_PART_ALL :
+ PUBLICATION_PART_ROOT);

This is similar to the above review comment. I'm not sure how you can
just assume that this must be the "except list" -- AFAICT this assumes
that 'GetAllPublicationRelations' can only be called by FOR ALL TABLES
(???). Seems like a lot of assumptions, that would be much better to
be enforced by Asserts in the code.

I agree with comments 7 and 8. I have added two functions
'GetPublicationIncludedRelations' and
'GetPublicationExcludedRelations'. To get Relations which are included
or excluded in a publication.
Both functions will call 'GetPublicationRelationsInternal' function. I
have also reintroduced the 'except_flag' variable

======
src/backend/commands/publicationcmds.c

pub_rf_contains_invalid_column:

9.
bool
pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
- bool pubviaroot)
+ bool pubviaroot, bool puballtables)

I felt that 'puballtables' is more "important" than 'pubviaroot' so
maybe it should come earlier in the parameter list. (e.g. make it more
similar to 'pub_contains_invalid_column')

======
src/backend/parser/gram.y

10.
+ pub_except_obj_list opt_except_clause

I felt that 'opt_except_clause' should better be called
'opt_pub_except_clause' or 'pub_opt_except_clause' because without
'pub' it is a bit vague.

I agree. I prefer 'opt_pub_except_clause'. By looking at other
variables it better make sense to start the variable name with 'opt_'
as it indicates that it is optional.
Made changes for the same.

~~~

11.
+/*
+ * ALL TABLES EXCEPT ( table_name [, ...] ) specification
+ */

11a
This comment should be up where all the other CREATE PUBLICATION
syntax is commented.

~

11b.
Also, there is a missing optional "[TABLE]" part.

~~~

12.
+pub_except_obj_list: PublicationExceptObjSpec
+ { $$ = list_make1($1); }
+ | pub_except_obj_list ',' PublicationExceptObjSpec
+ { $$ = lappend($1, $3); }
+ ;
+
+opt_except_clause:
+ EXCEPT opt_table '(' pub_except_obj_list ')' { $$ = $4; }
+ | /*EMPTY*/ { $$ = NIL; }
+ ;

I felt the clause should be defined before the obj list because that
seems the natural order to read these.

======
src/bin/pg_dump/pg_dump.c

13.
+static SimplePtrList exceptinfo = {NULL, NULL};

Having this as global seems a bit hacky. It has nothing in common with
all the other nearby lists, which are commented as being based on
"patterns given by command-line switches"

I agree, I have added it in the PublicationInfo struct and made the
corresponding code changes.

~~~

dumpPublication:

14.
+ /* Include exception tables if the publication has EXCEPT TABLEs */
+ for (SimplePtrListCell *cell = exceptinfo.head; cell; cell = cell->next)
+ {
+ PublicationRelInfo *pubrinfo = (PublicationRelInfo *) cell->ptr;
+ TableInfo  *tbinfo;
+
+ if (pubinfo == pubrinfo->publication)
+ {
+ tbinfo = pubrinfo->pubtable;

That 'tbinfo' can be declared within the "if".

~~~

15.
+ appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));

ONLY is not the default. How did you decide that "ONLY" is the correct
thing to do here?

For pg_dump for publication we use "ONLY" by default while specifying the table

For Alter publication we use similar thing:
```
appendPQExpBuffer(query, "ALTER PUBLICATION %s ADD TABLE ONLY",
fmtId(pubinfo->dobj.name));
```

Also if we specify a parent table in a publication(without ONLY) all
its child tables are also added to the pg_publication_rel table.
So when we dump such a publication we get something like:
.... EXCEPT TABLE(ONLY parent_table, ONLY child_table)...

~~~

getPublicationTables:

16.
- pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+ if (prexcept)
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_EXCEPT_REL;
+ else
+ pubrinfo[j].dobj.objType = DO_PUBLICATION_REL;
+

Would a single assignment (ternary) make this code simpler and easier to read?

SUGGESTION
pubrinfo[j].dobj.objType = prexcept ?
DO_PUBLICATION_EXCEPT_REL :
DO_PUBLICATION_REL;

======
src/bin/pg_dump/t/002_pg_dump.pl

17.
+ 'CREATE PUBLICATION pub10' => {
+ create_order => 50,
+ create_sql =>
+   'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE
(dump_test.test_table_generated);',
+ regexp => qr/^
+ \QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY
dump_test.test_table_generated, ONLY
dump_test.test_table_generated_child2, ONLY
dump_test.test_table_generated_child1) WITH (publish = 'insert,
update, delete, truncate');\E
+ /xm,
+ like => { %full_runs, section_post_data => 1, },
+ },
+

These "generated" names seem unusual. I saw there are some other
tables like 'dump_test.test_inheritance_child' and
'dump_test.test_inheritance_parent'. Can you use those more normal
table names instead?

Also curious - does the order of the tests matter? I saw that the
CREATE TABLE tests seem to be coming after the CREATE PUBLICATION
tests that are using them.

I looked into it and came to the conclusion that this is controlled
using 'create_order' while specifying the tests.
Tests with a lower create_order value are executed earlier.
So to ensure 'CREATE PUBLICATION' runs correctly we have to make sure
the 'create_order' of these statements is higher than that of the
respective 'CREATE TABLE' statement.

~~~
18.
- if (!defined($tests{$test}->{all_runs})
+ if (   !defined($tests{$test}->{all_runs})

Why add this whitespace?

pg_perltidy makes this change. I have reverted it.

======
src/include/nodes/parsenodes.h

19.
AP_SetObjects, /* set list of objects */
+ AP_Reset, /* reset the publication */
} AlterPublicationAction;

AFAIK, you removed all ALTER command changes from v30-0001. So this
should not be here.

~~~

20.
+ bool for_all_tables; /* Special publication for all tables in db */
AlterPublicationAction action; /* What action to perform with the given
* objects */
} AlterPublicationStmt;

AFAIK, you removed all ALTER command changes from v30-0001. So this
should not be here.

======
src/test/regress/sql/publication.sql

21.
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES
EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- specify EXCEPT without TABLE
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES
EXCEPT (testpub_tbl1);

Should be 2 comments here for the 2x CREATE:

# Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)

# Exclude tables using FOR ALL TABLES EXCEPT (tablelist)

~~~

22.
CREATE TABLE testpub_tbl3 (a int);
CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);

If you rename these tables like 'testpub_tbl_parent' and
'testpub_tbl_child', it will be much easier to see what is going on.

~~~

23.
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3);

Missing comment -- something like:
# Exclude parent table, omitting both of 'ONLY' and '*'

~~~

24.
+-- EXCEPT with wildcard: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl3*);

24a.
TBH, I don't think this is a "wildcard" -- it is not doing any pattern
matching. IMO just call it an "asterisk" or a "star".

~

24b.
And put a space before the '*' here.

======
.../t/037_rep_changes_except_table.pl

25.
+# ============================================
+# EXCEPT TABLE test cases for partition tables
+# ============================================
+# Check behavior of EXCEPT TABLE together with publish_via_partition_root
+# when applied to a partitioned table and its partitions.

Really, that "Check behavior" sentence is generic for all of the
following tests, so it should also be (within the "=======" of the
previous comment)

~~~

26.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+ CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+ INSERT INTO sch1.t1 VALUES (1), (6);
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE sch1.t1(a int);
+ CREATE TABLE sch1.part1(a int);
+ CREATE TABLE sch1.part2(a int);
+));

26a.
There should be a comment for this part that just says something like
"Setup partition table and partitions on the publisher that map to
normal tables on the subscriber"

~

26b.
The INSERT should be done later, after the CREATE PUBLICATION but
before the CREATE SUBSCRIPTION. The pattern will be the same for all
the test cases.

~~~

27.
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1)"
+);

Even though the publish_via_partition_root is 'false' by default, I
think you should spell it out explicitly here for clarity.

~~~

28.
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table while publish_via_partition_root = false
+# still allows rows inserted into its partitions to be replicated.

I felt you should word this differently. I don't think you should say
"inserted into its partitions" because actually, you inserted into the
partition table, and the data just ends up in the partitions.

~~~

29.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1);
+ INSERT INTO sch1.t1 VALUES (1), (6);
+));

Ditto earlier comment. Better to explicitly say
"publish_via_partition_root=false".

I have also addressed the remaining comments and attached the latest patch.

Thanks,
Shlok Kyal

Attachments:

v32-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v32-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From e5eb2fbaa7d74735c3b7536c4877c68bd928f2e6 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Dec 2025 22:41:23 +0530
Subject: [PATCH v32] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating a
publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/logical-replication.sgml         |   6 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          | 177 ++++++++++----
 src/backend/commands/publicationcmds.c        | 124 ++++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  33 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  27 +--
 src/backend/utils/cache/relcache.c            |  23 +-
 src/bin/pg_dump/pg_dump.c                     |  70 +++++-
 src/bin/pg_dump/pg_dump.h                     |   2 +
 src/bin/pg_dump/pg_dump_sort.c                |  10 +-
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 +++
 src/bin/psql/describe.c                       |  87 ++++++-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_publication.h          |  15 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/commands/publicationcmds.h        |   7 +-
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  75 +++++-
 src/test/regress/sql/publication.sql          |  33 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 218 ++++++++++++++++++
 24 files changed, 875 insertions(+), 150 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..9e847152b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation is excluded from the publication. See
+       <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index aa013f348d4..80512f87fda 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -116,7 +116,11 @@
    <literal>FOR TABLES IN SCHEMA</literal>, <literal>FOR ALL TABLES</literal>,
    or <literal>FOR ALL SEQUENCES</literal>. Unlike tables, sequences can be
    synchronized at any time. For more information, see
-   <xref linkend="logical-replication-sequences"/>.
+   <xref linkend="logical-replication-sequences"/>. When a publication is
+   created with <literal>FOR ALL TABLES</literal>, tables can be explicitly
+   excluded from publication using the <literal>EXCEPT TABLE</literal> clause.
+   See <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>
+   for more information.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..6b1c7f383e5 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">except_table_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>where <replaceable class="parameter">except_table_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,8 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. Tables listed in
+      EXCEPT TABLE are excluded from the publication.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +189,32 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. If <literal>ONLY</literal> is specified before the table
+      name, only that table is excluded from the publication. If
+      <literal>ONLY</literal> is not specified, the table and all its descendant
+      tables (if any) are excluded. Optionally, <literal>*</literal> can be
+      specified after the table name to explicitly indicate that descendant
+      tables are excluded. This does not apply to a partitioned table, however.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a root partitioned table has no
+      effect, as changes are replicated via the leaf partitions. Specifying a
+      leaf partition excludes only that partition from replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -487,6 +518,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES;
    all sequences for synchronization:
 <programlisting>
 CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all sequences and all
+   tables except tables <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT (users, departments);
 </programlisting>
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 7aa3f179924..86d9bf12a45 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -354,7 +354,8 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
  * ancestor is at the end of the list.
  */
 Oid
-GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level)
+GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
+								int *ancestor_level, bool puballtables)
 {
 	ListCell   *lc;
 	Oid			topmost_relid = InvalidOid;
@@ -366,32 +367,42 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
-		List	   *aschemaPubids = NIL;
+		List	   *apubids = NIL;
+		List	   *aexceptpubids = NIL;
+		bool		set_top = false;
+
+		GetRelationPublications(ancestor, &apubids, &aexceptpubids);
 
 		level++;
 
-		if (list_member_oid(apubids, puboid))
+		/* check if member of table publications */
+		set_top = list_member_oid(apubids, puboid);
+		if (!set_top)
 		{
-			topmost_relid = ancestor;
+			List	   *aschemapubids = GetSchemaPublications(get_rel_namespace(ancestor));
 
-			if (ancestor_level)
-				*ancestor_level = level;
+			/* check if member of schema publications */
+			set_top = list_member_oid(aschemapubids, puboid);
+			list_free(aschemapubids);
+
+			/*
+			 * If the publication is all tables publication and the table is
+			 * not part of exception tables.
+			 */
+			if (!set_top && puballtables)
+				set_top = !list_member_oid(aexceptpubids, puboid);
 		}
-		else
+
+		if (set_top)
 		{
-			aschemaPubids = GetSchemaPublications(get_rel_namespace(ancestor));
-			if (list_member_oid(aschemaPubids, puboid))
-			{
-				topmost_relid = ancestor;
+			topmost_relid = ancestor;
 
-				if (ancestor_level)
-					*ancestor_level = level;
-			}
+			if (ancestor_level)
+				*ancestor_level = level;
 		}
 
 		list_free(apubids);
-		list_free(aschemaPubids);
+		list_free(aexceptpubids);
 	}
 
 	return topmost_relid;
@@ -466,6 +477,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Handle the case where a partition is excluded by EXCEPT TABLE while
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relispartition)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
+	/*
+	 * Handle the case where a partitioned table is excluded by EXCEPT TABLE
+	 * while publish_via_partition_root = false.
+	 */
+	if (pub->alltables && !pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		ereport(WARNING,
+				(errmsg("partitioned table \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "false")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +513,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,38 +782,58 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
-List *
-GetRelationPublications(Oid relid)
+/*
+ * Get the list of publication oids associated with a specified relation.
+ *
+ * Parameter 'pubids' returns the OIDs of the publications the relation is part
+ * of. Parameter 'except_pubids' returns the OIDs of publications the relation
+ * is excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */
+bool
+GetRelationPublications(Oid relid, List **pubids, List **except_pubids)
 {
-	List	   *result = NIL;
 	CatCList   *pubrellist;
-	int			i;
+	bool		found = false;
 
 	/* Find all publications associated with the relation. */
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
-		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		Oid			pubid = pubrel->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (pubrel->prexcept)
+		{
+			if (except_pubids)
+				*except_pubids = lappend_oid(*except_pubids, pubid);
+		}
+		else
+		{
+			if (pubids)
+				*pubids = lappend_oid(*pubids, pubid);
+			found = true;
+		}
 	}
 
 	ReleaseSysCacheList(pubrellist);
 
-	return result;
+	return found;
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Internal function to get the list of relation OIDs for a publication.
  *
- * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * If except_flag is true, returns the list of relations excluded from the
+ * publication; otherwise, returns the list of relations included in the
+ * publication.
  */
-List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+static List *
+GetPublicationRelationsInternal(Oid pubid, PublicationPartOpt pub_partopt,
+								bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +858,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
 	}
 
 	systable_endscan(scan);
@@ -819,6 +874,34 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 	return result;
 }
 
+/*
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR TABLE publication, this returns the list of relations explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}
+
+/*
+ * Return the list of relation OIDs excluded from a publication.
+ * This is only applicable for FOR ALL TABLES publications.
+ */
+List *
+GetPublicationExcludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(GetPublication(pubid)->alltables);
+
+	return GetPublicationRelationsInternal(pubid, pub_partopt, true);
+}
+
 /*
  * Gets list of publication oids for publications marked as FOR ALL TABLES.
  */
@@ -864,15 +947,24 @@ GetAllTablesPublications(void)
  * partitioned tables, we must exclude partitions in favor of including the
  * root partitioned tables. This is not applicable to FOR ALL SEQUENCES
  * publication.
+ *
+ * The list does not include relations that are explicitly excluded via the
+ * EXCEPT TABLE clause of the publication specified by pubid.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist = NIL;
+
+	if (relkind == RELKIND_RELATION)
+		exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+													 PUBLICATION_PART_ALL :
+													 PUBLICATION_PART_ROOT);
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
@@ -891,7 +983,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +1005,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,17 +1262,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
 						   *schemarelids;
 
-				relids = GetPublicationRelations(pub_elem->oid,
-												 pub_elem->pubviaroot ?
-												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+				relids = GetPublicationIncludedRelations(pub_elem->oid,
+														 pub_elem->pubviaroot ?
+														 PUBLICATION_PART_ROOT :
+														 PUBLICATION_PART_LEAF);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1462,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a1983508950..b2fb724f048 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,38 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		switch (puballobj->pubobjtype)
+		{
+			case PUBLICATION_ALL_SEQUENCES:
+				break;
+			case PUBLICATION_ALL_TABLES:
+				foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_objects)
+				{
+					pubobj->pubtable->except = true;
+					*rels = lappend(*rels, pubobj->pubtable);
+				}
+				break;
+			default:
+				elog(ERROR, "invalid publication object type %d",
+					 puballobj->pubobjtype);
+				break;
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -194,6 +226,8 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 		switch (pubobj->pubobjtype)
 		{
 			case PUBLICATIONOBJ_TABLE:
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = (pubobj->pubobjtype == PUBLICATIONOBJ_EXCEPT_TABLE);
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -268,7 +302,7 @@ contain_invalid_rfcolumn_walker(Node *node, rf_context *context)
  */
 bool
 pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							   bool pubviaroot)
+							   bool puballtables, bool pubviaroot)
 {
 	HeapTuple	rftuple;
 	Oid			relid = RelationGetRelid(relation);
@@ -295,7 +329,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
 		publish_as_relid
-			= GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+			= GetTopMostAncestorInPublication(pubid, ancestors, NULL,
+											  puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -354,8 +389,8 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  */
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
-							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
+							bool puballtables, bool pubviaroot,
+							char pubgencols_type, bool *invalid_column_list,
 							bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
@@ -379,7 +414,8 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	 */
 	if (pubviaroot && relation->rd_rel->relispartition)
 	{
-		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL);
+		publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
+														   NULL, puballtables);
 
 		if (!OidIsValid(publish_as_relid))
 			publish_as_relid = relid;
@@ -514,8 +550,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * a target. However, WAL records for TRUNCATE specify both a root and
 		 * its leaves.
 		 */
-		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+		relids = GetPublicationIncludedRelations(pubid,
+												 PUBLICATION_PART_ALL);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -923,16 +959,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	/* Make the changes visible. */
 	CommandCounterIncrement();
 
-	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
 	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
@@ -944,22 +973,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
-
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
-
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
-
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
-
 		if (schemaidlist != NIL)
 		{
 			/*
@@ -971,8 +984,37 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 		}
 	}
 
+	/*
+	 * If publication is for ALL TABLES and relations is not empty, it means
+	 * that there are some relations to be excluded from the publication.
+	 * Else, relations is the list of relations to be added to the
+	 * publication.
+	 */
+	if (relations != NIL)
+	{
+		List	   *rels;
+
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
+
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
+
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
+
 	table_close(rel, RowExclusiveLock);
 
+	/* Associate objects with the publication. */
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1040,8 +1082,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
 						   AccessShareLock);
 
-		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+		root_relids = GetPublicationIncludedRelations(pubform->oid,
+													  PUBLICATION_PART_ROOT);
 
 		foreach(lc, root_relids)
 		{
@@ -1160,8 +1202,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 * trees, not just those explicitly mentioned in the publication.
 		 */
 		if (root_relids == NIL)
-			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+			relids = GetPublicationIncludedRelations(pubform->oid,
+													 PUBLICATION_PART_ALL);
 		else
 		{
 			/*
@@ -1246,8 +1288,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		PublicationDropTables(pubid, rels, false);
 	else						/* AP_SetObjects */
 	{
-		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+		List	   *oldrelids = GetPublicationIncludedRelations(pubid,
+																PUBLICATION_PART_ROOT);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1348,6 +1390,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc_object(PublicationRelInfo);
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1398,7 +1441,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationIncludedRelations(pubform->oid,
+												  PUBLICATION_PART_ROOT);
 
 		foreach(lc, reloids)
 		{
@@ -1761,6 +1805,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1833,6 +1878,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..3ea95ae1a26 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8687,7 +8687,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18882,7 +18882,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..f1220e55093 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,6 +455,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				pub_except_obj_list opt_pub_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -592,6 +593,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 <publicationobjectspec> PublicationExceptObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10787,7 +10789,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * pub_all_obj_type is one of:
  *
- *		TABLES
+ *		TABLES [EXCEPT [TABLE] ( table [, ...] )]
  *		SEQUENCES
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
@@ -10813,6 +10815,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10853,6 +10856,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10928,11 +10932,19 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_pub_except_clause:
+			EXCEPT opt_table '(' pub_except_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_pub_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_objects = $3;
+						if($$->except_objects != NULL)
+							preprocess_pubobj_list($$->except_objects, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10949,6 +10961,23 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+PublicationExceptObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+pub_except_obj_list: PublicationExceptObjSpec
+					{ $$ = list_make1($1); }
+			| pub_except_obj_list ',' PublicationExceptObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..d042da7b347 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = NIL;
+		List	   *exceptTablePubids = NIL;
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2099,6 +2100,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		char		relkind = get_rel_relkind(relid);
 		List	   *rel_publications = NIL;
 
+		GetRelationPublications(relid, &pubids, &exceptTablePubids);
+
 		/* Reload publications if needed before use. */
 		if (!publications_valid)
 		{
@@ -2195,22 +2198,6 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			Oid			pub_relid = relid;
 			int			ancestor_level = 0;
 
-			/*
-			 * If this is a FOR ALL TABLES publication, pick the partition
-			 * root and set the ancestor level accordingly.
-			 */
-			if (pub->alltables)
-			{
-				publish = true;
-				if (pub->pubviaroot && am_partition)
-				{
-					List	   *ancestors = get_partition_ancestors(relid);
-
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
-				}
-			}
-
 			if (!publish)
 			{
 				bool		ancestor_published = false;
@@ -2229,7 +2216,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
-															   &level);
+															   &level,
+															   pub->alltables);
 
 					if (ancestor != InvalidOid)
 					{
@@ -2244,6 +2232,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 				if (list_member_oid(pubids, pub->oid) ||
 					list_member_oid(schemaPubids, pub->oid) ||
+					(pub->alltables &&
+					 !list_member_oid(exceptTablePubids, pub->oid)) ||
 					ancestor_published)
 					publish = true;
 			}
@@ -2322,6 +2312,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptTablePubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2d0cb7bcfd4..f521fd949df 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5793,7 +5793,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
-	List	   *puboids;
+	List	   *puboids = NIL;
+	List	   *exceptpuboids = NIL;
+	List	   *alltablespuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	GetRelationPublications(relid, &puboids, &exceptpuboids);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5843,16 +5845,25 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		foreach(lc, ancestors)
 		{
 			Oid			ancestor = lfirst_oid(lc);
+			List	   *ancestor_puboids = NIL;
+			List	   *ancestor_exceptpuboids = NIL;
 
-			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+			GetRelationPublications(ancestor, &ancestor_puboids,
+									&ancestor_exceptpuboids);
+
+			puboids = list_concat_unique_oid(puboids, ancestor_puboids);
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   ancestor_exceptpuboids);
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
@@ -5883,6 +5894,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		if (!pubform->puballtables &&
 			(pubform->pubupdate || pubform->pubdelete) &&
 			pub_rf_contains_invalid_column(pubid, relation, ancestors,
+										   pubform->puballtables,
 										   pubform->pubviaroot))
 		{
 			if (pubform->pubupdate)
@@ -5899,6 +5911,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		 */
 		if ((pubform->pubupdate || pubform->pubdelete) &&
 			pub_contains_invalid_column(pubid, relation, ancestors,
+										pubform->puballtables,
 										pubform->pubviaroot,
 										pubform->pubgencols,
 										&invalid_column_list,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ad201af2f..d29c783ac5c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4634,9 +4634,48 @@ getPublications(Archive *fout)
 			(strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0);
 		pubinfo[i].pubgencols_type =
 			*(PQgetvalue(res, i, i_pubgencols));
+		pubinfo[i].excepttbls = (SimplePtrList)
+		{
+			NULL, NULL
+		};
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(pubinfo[i].dobj), fout);
+
+		if (fout->remoteVersion >= 190000)
+		{
+			int			ntbls;
+			PGresult   *res_tbls;
+
+			resetPQExpBuffer(query);
+			appendPQExpBuffer(query,
+							  "SELECT prrelid\n"
+							  "FROM pg_catalog.pg_publication_rel\n"
+							  "WHERE prpubid = %d and prexcept = true",
+							  pubinfo[i].dobj.catId.oid);
+
+			res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+			ntbls = PQntuples(res_tbls);
+			if (ntbls == 0)
+				continue;
+
+			for (int j = 0; j < ntbls; j++)
+			{
+				Oid			prrelid;
+				TableInfo  *tbinfo;
+
+				prrelid = atooid(PQgetvalue(res_tbls, j, 0));
+
+				tbinfo = findTableByOid(prrelid);
+				if (tbinfo == NULL)
+					continue;
+
+				simple_ptr_list_append(&pubinfo[i].excepttbls, tbinfo);
+			}
+
+			PQclear(res_tbls);
+		}
 	}
 
 cleanup:
@@ -4676,7 +4715,28 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		bool		first_tbl = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has EXCEPT TABLEs */
+		for (SimplePtrListCell *cell = pubinfo->excepttbls.head; cell; cell = cell->next)
+		{
+			TableInfo  *tbinfo = (TableInfo *) cell->ptr;
+
+			if (first_tbl)
+			{
+				appendPQExpBufferStr(query, " EXCEPT TABLE (");
+				first_tbl = false;
+			}
+			else
+				appendPQExpBufferStr(query, ", ");
+			appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+		}
+		if (!first_tbl)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4856,8 +4916,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
+							 "SELECT tableoid, oid, prpubid, prrelid,\n"
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4868,6 +4929,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " WHERE prexcept = false");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
@@ -11826,6 +11890,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_PUBLICATION:
 			dumpPublication(fout, (const PublicationInfo *) dobj);
 			break;
+		case DO_PUBLICATION_EXCEPT_REL:
+			/* will be dumped in dumpPublication */
+			break;
 		case DO_PUBLICATION_REL:
 			dumpPublicationTable(fout, (const PublicationRelInfo *) dobj);
 			break;
@@ -20196,6 +20263,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_DEFAULT_ACL:
 			case DO_POLICY:
 			case DO_PUBLICATION:
+			case DO_PUBLICATION_EXCEPT_REL:
 			case DO_PUBLICATION_REL:
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..b85e250c203 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -81,6 +81,7 @@ typedef enum
 	DO_REFRESH_MATVIEW,
 	DO_POLICY,
 	DO_PUBLICATION,
+	DO_PUBLICATION_EXCEPT_REL,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_REL_STATS,
@@ -676,6 +677,7 @@ typedef struct _PublicationInfo
 	bool		pubtruncate;
 	bool		pubviaroot;
 	PublishGencolsType pubgencols_type;
+	SimplePtrList excepttbls;
 } PublicationInfo;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 164c76e0864..6ebeb9c96a1 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -92,6 +92,7 @@ enum dbObjectTypePriorities
 	PRIO_FK_CONSTRAINT,
 	PRIO_POLICY,
 	PRIO_PUBLICATION,
+	PRIO_PUBLICATION_EXCEPT_REL,
 	PRIO_PUBLICATION_REL,
 	PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	PRIO_SUBSCRIPTION,
@@ -147,6 +148,7 @@ static const int dbObjectTypePriority[] =
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
+	[DO_PUBLICATION_EXCEPT_REL] = PRIO_PUBLICATION_EXCEPT_REL,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
 	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
@@ -432,7 +434,8 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
-	else if (obj1->objType == DO_PUBLICATION_REL)
+	else if (obj1->objType == DO_PUBLICATION_REL ||
+			 obj1->objType == DO_PUBLICATION_EXCEPT_REL)
 	{
 		PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1;
 		PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2;
@@ -1715,6 +1718,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "PUBLICATION (ID %d OID %u)",
 					 obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PUBLICATION_EXCEPT_REL:
+			snprintf(buf, bufsize,
+					 "PUBLICATION EXCEPT TABLE (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PUBLICATION_REL:
 			snprintf(buf, bufsize,
 					 "PUBLICATION TABLE (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..a3fcf3c2b0a 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,36 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub10' => {
+		create_order => 92,
+		create_sql =>
+		  'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_inheritance_parent);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_inheritance_parent, ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..8cd73b3ad53 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -3134,6 +3151,35 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* Print publication the relation is excluded explicitly */
+		if (pset.sversion >= 190000)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT pubname\n"
+							  "FROM pg_catalog.pg_publication p\n"
+							  "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+							  "WHERE pr.prrelid = '%s'\n AND pr.prexcept\n"
+							  "ORDER BY 1;", oid);
+
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+				printTableAddFooter(&cont, _("Except Publications:"));
+
+			/* Might be an empty set - that's ok */
+			for (i = 0; i < tuples; i++)
+			{
+				printfPQExpBuffer(&buf, "    \"%s\"", PQgetvalue(result, i, 0));
+
+				printTableAddFooter(&cont, buf.data);
+			}
+			PQclear(result);
+		}
+
 		/*
 		 * If verbose, print NOT NULL constraints.
 		 */
@@ -6753,8 +6799,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6822,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b1ff6f6cd94..ab5c9991b51 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3637,7 +3637,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..76ce2de4773 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,14 +146,16 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern bool GetRelationPublications(Oid relid, List **pubids, List **except_pubids);
 
 /*---------
- * Expected values for pub_partopt parameter of GetPublicationRelations(),
+ * Expected values for pub_partopt parameter of
+ * GetPublicationIncludedRelations(), and GetPublicationExcludedRelations(),
  * which allows callers to specify which partitions of partitioned tables
  * mentioned in the publication they expect to see.
  *
@@ -168,9 +170,12 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationIncludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
+extern List *GetPublicationExcludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
@@ -181,7 +186,7 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											PublicationPartOpt pub_partopt,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
-											int *ancestor_level);
+											int *ancestor_level, bool puballtables);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index f90cf1ef896..4a170994f76 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -32,10 +32,11 @@ extern ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId);
 extern void AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId);
 extern void InvalidatePublicationRels(List *relids);
 extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
-										   List *ancestors, bool pubviaroot);
+										   List *ancestors, bool pubviaroot,
+										   bool puballtables);
 extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
-										List *ancestors, bool pubviaroot,
-										char pubgencols_type,
+										List *ancestors, bool puballtables,
+										bool pubviaroot, char pubgencols_type,
 										bool *invalid_column_list,
 										bool *invalid_gen_col);
 extern void InvalidatePubRelSyncCache(Oid pubid, bool puballtables);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..2d829563caf 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4299,6 +4299,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4307,6 +4308,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4335,6 +4337,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_objects; /* List of publication object to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..31a3d6f471d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,42 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+-- EXCEPT with asterisk: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -227,8 +256,8 @@ RESET client_min_messages;
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
-    "public.testpub_tbl3a"
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
 
 \dRp+ testpub4
                                                       Publication testpub4
@@ -236,10 +265,36 @@ Tables:
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
+    "public.testpub_tbl_parent"
+
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+\dRp+ testpub7
+                                                      Publication testpub7
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_parent"
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..6817422356d 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,39 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+-- EXCEPT with asterisk: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
+\dRp+ testpub7
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..30ffb2a339f
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,218 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT TABLE publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# ============================================
+# EXCEPT TABLE test cases for normal tables
+# ============================================
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Verify that data inserted to the excluded table is not replicated.
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+# ============================================
+# EXCEPT TABLE test cases for partitioned tables
+# Check behavior of EXCEPT TABLE with publish_via_partition_root on a
+# partitioned table and its partitions.
+# ============================================
+# Setup partitioned table and partitions on the publisher that map to normal
+# tables on the subscriber
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+	CREATE TABLE sch1.part2(a int);
+));
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = false
+# Excluding a partition while publish_via_partition_root = false prevents
+# replication of rows inserted into the partitioned table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on other partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table still allows rows inserted into the
+# partitioned table to be replicated via its partitions.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = true
+# When the partitioned table is excluded and publish_via_partition_root is true,
+# no rows from the table or its partitions are replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = true
+# When a partition is excluded but publish_via_partition_root is true,
+# rows published through the partitioned table can still be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1 ORDER BY a");
+is( $result, qq(1
+2
+6
+7), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on other partition');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#163Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#161)
Re: Skipping schema changes in publication

On Thu, 11 Dec 2025 at 04:31, Peter Smith <smithpb2250@gmail.com> wrote:

On Wed, Dec 10, 2025 at 4:49 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Mon, 24 Nov 2025 at 13:03, Peter Smith <smithpb2250@gmail.com> wrote:

...

21.
I was wondering if the "describe" for tables (e.g. \d+) should also
show the publications where the table is an ECEPT TABLE? How else is
the user going to know it has been excluded by some publication?

I thought it would be sufficient to show only the list of
publications, the table is part of.
Users can check the excluded tables by checking the description of the
publication using \dRp+.
Will it be not sufficient?
I am not sure why we should show a list of publications which it is not part of?
Am I missing something thoughts?

For this comment, I was imagining a scenario where there are dozens of
publications, and the user is wondering why their table is not being
replicated to the subscriber like they expected it would be.

Yes, they could use \dRs+ to identify the publications excluding it,
but that will be quite painful if there are very many publications
they have to check. IIUC, there is no other way to check it without
digging into System Catalogs.

That's why I thought it might be useful if the \d+ could also show
publications where the table was named in an EXCEPT TABLE clause.

I thought more about this point and it can be useful. I have added the
changes for the same in the latest patch in [1]/messages/by-id/CANhcyEWg2WbEW_fFwk0D3J2KBrUF7th6VrE+gvESgkUKP9VpZg@mail.gmail.com.

[1]: /messages/by-id/CANhcyEWg2WbEW_fFwk0D3J2KBrUF7th6VrE+gvESgkUKP9VpZg@mail.gmail.com

Thanks,
Shlok Kyal

#164shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#162)
Re: Skipping schema changes in publication

On Tue, Dec 16, 2025 at 2:50 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have also addressed the remaining comments and attached the latest patch.

Thanks. A few comments:

1)
+ if (!set_top && puballtables)
+ set_top = !list_member_oid(aexceptpubids, puboid);

In GetTopMostAncestorInPublication(), we have made the above change
which will now get ancestor from all-tables publication as well,
provided table is not part of 'except' List. Earlier this function was
only checking pg_subscription_rel and pg_publication_namespace which
does not include all-tables publication. Won't it change the
result-set for callers?

2)
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+ return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}

We can have an Assert here that pubid passed is not for ALL-Tables or
ALL-sequences

3)
GetAllPublicationRelations:
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. This is not applicable to FOR ALL SEQUENCES
* publication.

+ * The list does not include relations that are explicitly excluded via the
+ * EXCEPT TABLE clause of the publication specified by pubid.

Suggestion:
/*
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. The list also excludes tables that are
* explicitly excluded via the EXCEPT TABLE clause of the publication
* identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
* publications.
*/

4)
GetAllPublicationRelations:
+ if (relkind == RELKIND_RELATION)
+ exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+ PUBLICATION_PART_ALL :
+ PUBLICATION_PART_ROOT);

Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));

Generally we keep such parameters' sanity checks as the first step. We
can add new code after Assert.

5)
ObjectsInAllPublicationToOids() only has one caller which calls it
under check: 'if (stmt->for_all_tables)'

Thus IMO, we do not need a switch-case in
ObjectsInAllPublicationToOids(). We can simply have a sanity check to
see it is 'PUBLICATION_ALL_TABLES' and then do the needed operation
for this pub-type.

6)
CreatePublication():
/*
* If publication is for ALL TABLES and relations is not empty, it means
* that there are some relations to be excluded from the publication.
* Else, relations is the list of relations to be added to the
* publication.
*/

Shall we rephrase slightly to:

/*
* If the publication is for ALL TABLES and 'relations' is not empty,
* it indicates that some relations should be excluded from the publication.
* Add those excluded relations to the publication with 'prexcept' set to true.
* Otherwise, 'relations' contains the list of relations to be explicitly
* included in the publication.
*/

7)
+ /* Associate objects with the publication. */
+ if (stmt->for_all_tables)
+ {
+ /* Invalidate relcache so that publication info is rebuilt. */
+ CacheInvalidateRelcacheAll();
+ }

I think this comment is misplaced. We shall have it at previous place, atop:
if (stmt->for_all_tables)
This is because here we are just trying to invalidate cache while at
previous place we are trying to associate.

thanks
Shveta

#165shveta malik
shveta.malik@gmail.com
In reply to: shveta malik (#164)
Re: Skipping schema changes in publication

On Wed, Dec 17, 2025 at 11:24 AM shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Dec 16, 2025 at 2:50 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have also addressed the remaining comments and attached the latest patch.

Thanks. A few comments:

1)
+ if (!set_top && puballtables)
+ set_top = !list_member_oid(aexceptpubids, puboid);

In GetTopMostAncestorInPublication(), we have made the above change
which will now get ancestor from all-tables publication as well,
provided table is not part of 'except' List. Earlier this function was
only checking pg_subscription_rel and pg_publication_namespace which
does not include all-tables publication. Won't it change the
result-set for callers?

2)
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+ return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}

We can have an Assert here that pubid passed is not for ALL-Tables or
ALL-sequences

3)
GetAllPublicationRelations:
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. This is not applicable to FOR ALL SEQUENCES
* publication.

+ * The list does not include relations that are explicitly excluded via the
+ * EXCEPT TABLE clause of the publication specified by pubid.

Suggestion:
/*
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. The list also excludes tables that are
* explicitly excluded via the EXCEPT TABLE clause of the publication
* identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
* publications.
*/

4)
GetAllPublicationRelations:
+ if (relkind == RELKIND_RELATION)
+ exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+ PUBLICATION_PART_ALL :
+ PUBLICATION_PART_ROOT);

Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));

Generally we keep such parameters' sanity checks as the first step. We
can add new code after Assert.

5)
ObjectsInAllPublicationToOids() only has one caller which calls it
under check: 'if (stmt->for_all_tables)'

Thus IMO, we do not need a switch-case in
ObjectsInAllPublicationToOids(). We can simply have a sanity check to
see it is 'PUBLICATION_ALL_TABLES' and then do the needed operation
for this pub-type.

6)
CreatePublication():
/*
* If publication is for ALL TABLES and relations is not empty, it means
* that there are some relations to be excluded from the publication.
* Else, relations is the list of relations to be added to the
* publication.
*/

Shall we rephrase slightly to:

/*
* If the publication is for ALL TABLES and 'relations' is not empty,
* it indicates that some relations should be excluded from the publication.
* Add those excluded relations to the publication with 'prexcept' set to true.
* Otherwise, 'relations' contains the list of relations to be explicitly
* included in the publication.
*/

7)
+ /* Associate objects with the publication. */
+ if (stmt->for_all_tables)
+ {
+ /* Invalidate relcache so that publication info is rebuilt. */
+ CacheInvalidateRelcacheAll();
+ }

I think this comment is misplaced. We shall have it at previous place, atop:
if (stmt->for_all_tables)
This is because here we are just trying to invalidate cache while at
previous place we are trying to associate.

Few more:

8)
get_rel_sync_entry()
+ List *exceptTablePubids = NIL;

At all other places, we are using exceptpubids, shall we use the same here?

9)
ObjectsInPublicationToOids()

  case PUBLICATIONOBJ_TABLE:
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = (pubobj->pubobjtype ==
PUBLICATIONOBJ_EXCEPT_TABLE);
  *rels = lappend(*rels, pubobj->pubtable);
  break;

It looks slightly odd that for pubobjtype case
'PUBLICATIONOBJ_EXCEPT_TABLE', we have to check pubobjtype against
PUBLICATIONOBJ_EXCEPT_TABLE itself.

Shall we make it:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = true;
/* fall through */
case PUBLICATIONOBJ_TABLE:
*rels = lappend(*rels, pubobj->pubtable);
break;

10)
I want to understand the usage of DO_PUBLICATION_EXCEPT_REL. Can you
give a scenario where its usage in DOTypeNameCompare() will be hit?
Its all other usages too need some analysis and validation.

11)
+ List *except_objects; /* List of publication object to be excluded */

object --> objects
Currently since we exclude only tables, does it make sense to name it
as except_tables?

thanks
Shveta

#166Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#165)
1 attachment(s)
Re: Skipping schema changes in publication

On Thu, 18 Dec 2025 at 11:30, shveta malik <shveta.malik@gmail.com> wrote:

On Wed, Dec 17, 2025 at 11:24 AM shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Dec 16, 2025 at 2:50 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have also addressed the remaining comments and attached the latest patch.

Thanks. A few comments:

1)
+ if (!set_top && puballtables)
+ set_top = !list_member_oid(aexceptpubids, puboid);

In GetTopMostAncestorInPublication(), we have made the above change
which will now get ancestor from all-tables publication as well,
provided table is not part of 'except' List. Earlier this function was
only checking pg_subscription_rel and pg_publication_namespace which
does not include all-tables publication. Won't it change the
result-set for callers?

It can change the result set of callers. I analysed more and saw that
GetTopMostAncestorInPublication is called from 3 places.
1. pub_rf_contains_invalid_column: it is called when publication is
not ALL TABLES. It will have no impact with the change.
2. pub_contains_invalid_column : it is called for all type of
publication. it calls GetTopMostAncestorInPublication like:
```
if (pubviaroot && relation->rd_rel->relispartition)
{
publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
NULL, puballtables);

if (!OidIsValid(publish_as_relid))
publish_as_relid = relid;
}
```
In HEAD for ALL TABLES publication GetTopMostAncestorInPublication
will always return InvalidOid. With this patch it can have some value.
So there is a difference in behaviour.

3. get_rel_sync_entry
in HEAD we had
```
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
```
With patch this condition is not valid because we cannot set
'pub_relid = llast_oid(ancestors);' directly as the table can be
excluded.
So, the change in GetTopMostAncestorInPublication will even handle the
case of "ALL TABLES" publication.

Since we have a behaviour difference for the 2nd function, I have
removed the changes for 'ALL TABLES' from
GetTopMostAncestorInPublication and added it separately
'get_rel_sync_entry'. Thoughts?

2)
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+ return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}

We can have an Assert here that pubid passed is not for ALL-Tables or
ALL-sequences

Added assert for all tables. I found during testing that this function
can be called for ALL SEQUENCES in HEAD. So I have not added an
assertion for it.
I think it is a bug and shared the same in [1]/messages/by-id/CALDaNm0qoNtsX+9KPug6qb=uC-H2iPMYW+gL=Hehx+NgOxga6w@mail.gmail.com. Will add assert for
all sequences as well once the bug is fixed.

3)
GetAllPublicationRelations:
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. This is not applicable to FOR ALL SEQUENCES
* publication.

+ * The list does not include relations that are explicitly excluded via the
+ * EXCEPT TABLE clause of the publication specified by pubid.

Suggestion:
/*
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. The list also excludes tables that are
* explicitly excluded via the EXCEPT TABLE clause of the publication
* identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
* publications.
*/

4)
GetAllPublicationRelations:
+ if (relkind == RELKIND_RELATION)
+ exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+ PUBLICATION_PART_ALL :
+ PUBLICATION_PART_ROOT);

Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));

Generally we keep such parameters' sanity checks as the first step. We
can add new code after Assert.

5)
ObjectsInAllPublicationToOids() only has one caller which calls it
under check: 'if (stmt->for_all_tables)'

Thus IMO, we do not need a switch-case in
ObjectsInAllPublicationToOids(). We can simply have a sanity check to
see it is 'PUBLICATION_ALL_TABLES' and then do the needed operation
for this pub-type.

6)
CreatePublication():
/*
* If publication is for ALL TABLES and relations is not empty, it means
* that there are some relations to be excluded from the publication.
* Else, relations is the list of relations to be added to the
* publication.
*/

Shall we rephrase slightly to:

/*
* If the publication is for ALL TABLES and 'relations' is not empty,
* it indicates that some relations should be excluded from the publication.
* Add those excluded relations to the publication with 'prexcept' set to true.
* Otherwise, 'relations' contains the list of relations to be explicitly
* included in the publication.
*/

7)
+ /* Associate objects with the publication. */
+ if (stmt->for_all_tables)
+ {
+ /* Invalidate relcache so that publication info is rebuilt. */
+ CacheInvalidateRelcacheAll();
+ }

I think this comment is misplaced. We shall have it at previous place, atop:
if (stmt->for_all_tables)
This is because here we are just trying to invalidate cache while at
previous place we are trying to associate.

Few more:

8)
get_rel_sync_entry()
+ List *exceptTablePubids = NIL;

At all other places, we are using exceptpubids, shall we use the same here?

9)
ObjectsInPublicationToOids()

case PUBLICATIONOBJ_TABLE:
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = (pubobj->pubobjtype ==
PUBLICATIONOBJ_EXCEPT_TABLE);
*rels = lappend(*rels, pubobj->pubtable);
break;

It looks slightly odd that for pubobjtype case
'PUBLICATIONOBJ_EXCEPT_TABLE', we have to check pubobjtype against
PUBLICATIONOBJ_EXCEPT_TABLE itself.

Shall we make it:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = true;
/* fall through */
case PUBLICATIONOBJ_TABLE:
*rels = lappend(*rels, pubobj->pubtable);
break;

We should also make pubobj->pubtable->except = false for PUBLICATIONOBJ_TABLE?
Updated the condition like:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = true;
*rels = lappend(*rels, pubobj->pubtable);
break;
case PUBLICATIONOBJ_TABLE:
pubobj->pubtable->except = false;
*rels = lappend(*rels, pubobj->pubtable);
break;

10)
I want to understand the usage of DO_PUBLICATION_EXCEPT_REL. Can you
give a scenario where its usage in DOTypeNameCompare() will be hit?
Its all other usages too need some analysis and validation.

In the current patch we are not setting an objecttype to
DO_PUBLICATION_EXCEPT_REL.
We are storing the list of except tables in 'pubinfo[i].excepttbls'
list in function getPublications and "pubinfo[i].dobj.objType =
DO_PUBLICATION". So, I don't see any requirement of
DO_PUBLICATION_EXCEPT_REL now. I have removed it.

11)
+ List *except_objects; /* List of publication object to be excluded */

object --> objects
Currently since we exclude only tables, does it make sense to name it
as except_tables?

I have also addressed the remaining comments and attached the updated v33 patch.
[1]: /messages/by-id/CALDaNm0qoNtsX+9KPug6qb=uC-H2iPMYW+gL=Hehx+NgOxga6w@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v33-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v33-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 1275f1e2c8056cc59a2a91f2ee46d21d00e21d99 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Dec 2025 22:41:23 +0530
Subject: [PATCH v33] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE/ALTER PUBLICATION allows one or
more tables to be excluded. The publisher will not send the data of
excluded tables to the subscriber.

The new syntax allows specifying excluded relations when creating a
publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables. e.g. psql
\dRp+ variant will now display associated "except tables" if any.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/logical-replication.sgml         |   6 +-
 doc/src/sgml/ref/create_publication.sgml      |  52 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   5 +-
 src/backend/catalog/pg_publication.c          | 151 +++++++++---
 src/backend/commands/publicationcmds.c        | 109 ++++++---
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  33 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  22 +-
 src/backend/utils/cache/relcache.c            |  21 +-
 src/bin/pg_dump/pg_dump.c                     |  66 +++++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 +++
 src/bin/psql/describe.c                       |  87 ++++++-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_publication.h          |  13 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  75 +++++-
 src/test/regress/sql/publication.sql          |  33 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 219 ++++++++++++++++++
 22 files changed, 841 insertions(+), 113 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..9e847152b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation is excluded from the publication. See
+       <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index aa013f348d4..80512f87fda 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -116,7 +116,11 @@
    <literal>FOR TABLES IN SCHEMA</literal>, <literal>FOR ALL TABLES</literal>,
    or <literal>FOR ALL SEQUENCES</literal>. Unlike tables, sequences can be
    synchronized at any time. For more information, see
-   <xref linkend="logical-replication-sequences"/>.
+   <xref linkend="logical-replication-sequences"/>. When a publication is
+   created with <literal>FOR ALL TABLES</literal>, tables can be explicitly
+   excluded from publication using the <literal>EXCEPT TABLE</literal> clause.
+   See <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>
+   for more information.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..6b1c7f383e5 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">except_table_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>where <replaceable class="parameter">except_table_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,8 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. Tables listed in
+      EXCEPT TABLE are excluded from the publication.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +189,32 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication. If <literal>ONLY</literal> is specified before the table
+      name, only that table is excluded from the publication. If
+      <literal>ONLY</literal> is not specified, the table and all its descendant
+      tables (if any) are excluded. Optionally, <literal>*</literal> can be
+      specified after the table name to explicitly indicate that descendant
+      tables are excluded. This does not apply to a partitioned table, however.
+     </para>
+     <para>
+      When <literal>publish_via_partition_root</literal> is set to
+      <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a root partitioned table has no
+      effect, as changes are replicated via the leaf partitions. Specifying a
+      leaf partition excludes only that partition from replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -487,6 +518,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES;
    all sequences for synchronization:
 <programlisting>
 CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all sequences and all
+   tables except tables <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT (users, departments);
 </programlisting>
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..f1b3ce380b6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 7aa3f179924..0abd2fc8b5c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -366,11 +366,20 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = NIL;
+		List	   *aexceptpubids = NIL;
 		List	   *aschemaPubids = NIL;
 
+		GetRelationPublications(ancestor, &apubids, &aexceptpubids);
+
 		level++;
 
+		if (list_member_oid(aexceptpubids, puboid))
+		{
+			list_free(aexceptpubids);
+			continue;
+		}
+
 		if (list_member_oid(apubids, puboid))
 		{
 			topmost_relid = ancestor;
@@ -391,6 +400,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		}
 
 		list_free(apubids);
+		list_free(aexceptpubids);
 		list_free(aschemaPubids);
 	}
 
@@ -466,6 +476,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Handle the case where a partition is excluded by EXCEPT TABLE while
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relispartition)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
+	/*
+	 * Handle the case where a partitioned table is excluded by EXCEPT TABLE
+	 * while publish_via_partition_root = false.
+	 */
+	if (pub->alltables && !pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		ereport(WARNING,
+				(errmsg("partitioned table \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "false")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +512,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,38 +781,58 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
-List *
-GetRelationPublications(Oid relid)
+/*
+ * Get the list of publication oids associated with a specified relation.
+ *
+ * Parameter 'pubids' returns the OIDs of the publications the relation is part
+ * of. Parameter 'except_pubids' returns the OIDs of publications the relation
+ * is excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */
+bool
+GetRelationPublications(Oid relid, List **pubids, List **except_pubids)
 {
-	List	   *result = NIL;
 	CatCList   *pubrellist;
-	int			i;
+	bool		found = false;
 
 	/* Find all publications associated with the relation. */
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
-		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		Oid			pubid = pubrel->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (pubrel->prexcept)
+		{
+			if (except_pubids)
+				*except_pubids = lappend_oid(*except_pubids, pubid);
+		}
+		else
+		{
+			if (pubids)
+				*pubids = lappend_oid(*pubids, pubid);
+			found = true;
+		}
 	}
 
 	ReleaseSysCacheList(pubrellist);
 
-	return result;
+	return found;
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Internal function to get the list of relation OIDs for a publication.
  *
- * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * If except_flag is true, returns the list of relations excluded from the
+ * publication; otherwise, returns the list of relations included in the
+ * publication.
  */
-List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+static List *
+GetPublicationRelationsInternal(Oid pubid, PublicationPartOpt pub_partopt,
+								bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +857,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
 	}
 
 	systable_endscan(scan);
@@ -819,6 +873,36 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 	return result;
 }
 
+/*
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR TABLE publication, this returns the list of relations explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(!GetPublication(pubid)->alltables);
+
+	return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}
+
+/*
+ * Return the list of relation OIDs excluded from a publication.
+ * This is only applicable for FOR ALL TABLES publications.
+ */
+List *
+GetPublicationExcludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(GetPublication(pubid)->alltables);
+
+	return GetPublicationRelationsInternal(pubid, pub_partopt, true);
+}
+
 /*
  * Gets list of publication oids for publications marked as FOR ALL TABLES.
  */
@@ -862,20 +946,28 @@ GetAllTablesPublications(void)
  *
  * If the publication publishes partition changes via their respective root
  * partitioned tables, we must exclude partitions in favor of including the
- * root partitioned tables. This is not applicable to FOR ALL SEQUENCES
- * publication.
+ * root partitioned tables. The list also excludes tables that are
+ * explicitly excluded via the EXCEPT TABLE clause of the publication
+ * identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
+ * publications.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist = NIL;
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
+	if (relkind == RELKIND_RELATION)
+		exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+													 PUBLICATION_PART_ALL :
+													 PUBLICATION_PART_ROOT);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -891,7 +983,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +1005,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,17 +1262,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+															 RELKIND_RELATION,
 															 pub_elem->pubviaroot);
 			else
 			{
 				List	   *relids,
 						   *schemarelids;
 
-				relids = GetPublicationRelations(pub_elem->oid,
-												 pub_elem->pubviaroot ?
-												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+				relids = GetPublicationIncludedRelations(pub_elem->oid,
+														 pub_elem->pubviaroot ?
+														 PUBLICATION_PART_ROOT :
+														 PUBLICATION_PART_LEAF);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1462,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication->oid, RELKIND_SEQUENCE, false);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a1983508950..f76b968c04a 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,30 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		if (puballobj->pubobjtype != PUBLICATION_ALL_TABLES)
+			continue;
+
+		foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_tables)
+		{
+			pubobj->pubtable->except = true;
+			*rels = lappend(*rels, pubobj->pubtable);
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -193,7 +217,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 
 		switch (pubobj->pubobjtype)
 		{
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -355,8 +384,7 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 							bool pubviaroot, char pubgencols_type,
-							bool *invalid_column_list,
-							bool *invalid_gen_col)
+							bool *invalid_column_list, bool *invalid_gen_col)
 {
 	Oid			relid = RelationGetRelid(relation);
 	Oid			publish_as_relid = RelationGetRelid(relation);
@@ -514,8 +542,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * a target. However, WAL records for TRUNCATE specify both a root and
 		 * its leaves.
 		 */
-		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+		relids = GetPublicationIncludedRelations(pubid,
+												 PUBLICATION_PART_ALL);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -925,14 +953,8 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 
 	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
 	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
@@ -944,22 +966,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
-
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
-
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
-
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
-
 		if (schemaidlist != NIL)
 		{
 			/*
@@ -971,8 +977,37 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 		}
 	}
 
+	/*
+	 * If the publication is for ALL TABLES and 'relations' is not empty, it
+	 * indicates that some relations should be excluded from the publication.
+	 * Add those excluded relations to the publication with 'prexcept' set to
+	 * true. Otherwise, 'relations' contains the list of relations to be
+	 * explicitly included in the publication.
+	 */
+	if (relations != NIL)
+	{
+		List	   *rels;
+
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
+
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
+
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
+
 	table_close(rel, RowExclusiveLock);
 
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1040,8 +1075,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
 						   AccessShareLock);
 
-		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+		root_relids = GetPublicationIncludedRelations(pubform->oid,
+													  PUBLICATION_PART_ROOT);
 
 		foreach(lc, root_relids)
 		{
@@ -1160,8 +1195,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 * trees, not just those explicitly mentioned in the publication.
 		 */
 		if (root_relids == NIL)
-			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+			relids = GetPublicationIncludedRelations(pubform->oid,
+													 PUBLICATION_PART_ALL);
 		else
 		{
 			/*
@@ -1246,8 +1281,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		PublicationDropTables(pubid, rels, false);
 	else						/* AP_SetObjects */
 	{
-		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+		List	   *oldrelids = GetPublicationIncludedRelations(pubid,
+																PUBLICATION_PART_ROOT);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1348,6 +1383,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc_object(PublicationRelInfo);
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1398,7 +1434,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationIncludedRelations(pubform->oid,
+												  PUBLICATION_PART_ROOT);
 
 		foreach(lc, reloids)
 		{
@@ -1761,6 +1798,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1833,6 +1871,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..3ea95ae1a26 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8687,7 +8687,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18882,7 +18882,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..bcf0bc57aa4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,6 +455,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				pub_except_obj_list opt_pub_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -592,6 +593,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 <publicationobjectspec> PublicationExceptObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10787,7 +10789,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * pub_all_obj_type is one of:
  *
- *		TABLES
+ *		TABLES [EXCEPT [TABLE] ( table [, ...] )]
  *		SEQUENCES
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
@@ -10813,6 +10815,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10853,6 +10856,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10928,11 +10932,19 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_pub_except_clause:
+			EXCEPT opt_table '(' pub_except_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_pub_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_tables = $3;
+						if($$->except_tables != NULL)
+							preprocess_pubobj_list($$->except_tables, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10949,6 +10961,23 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+PublicationExceptObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+pub_except_obj_list: PublicationExceptObjSpec
+					{ $$ = list_make1($1); }
+			| pub_except_obj_list ',' PublicationExceptObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..88174bd299a 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = NIL;
+		List	   *exceptpubids = NIL;
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2099,6 +2100,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		char		relkind = get_rel_relkind(relid);
 		List	   *rel_publications = NIL;
 
+		GetRelationPublications(relid, &pubids, &exceptpubids);
+
 		/* Reload publications if needed before use. */
 		if (!publications_valid)
 		{
@@ -2205,9 +2208,21 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				if (pub->pubviaroot && am_partition)
 				{
 					List	   *ancestors = get_partition_ancestors(relid);
+					int			level = 0;
+
+					foreach_oid(ancestor, ancestors)
+					{
+						List	   *aexceptpubids = NIL;
 
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
+						level++;
+						GetRelationPublications(ancestor, NULL, &aexceptpubids);
+
+						if (!list_member_oid(aexceptpubids, pub->oid))
+						{
+							pub_relid = ancestor;
+							ancestor_level = level;
+						}
+					}
 				}
 			}
 
@@ -2322,6 +2337,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptpubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2d0cb7bcfd4..bc5f9495923 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5793,7 +5793,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
-	List	   *puboids;
+	List	   *puboids = NIL;
+	List	   *exceptpuboids = NIL;
+	List	   *alltablespuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	GetRelationPublications(relid, &puboids, &exceptpuboids);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5843,16 +5845,25 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		foreach(lc, ancestors)
 		{
 			Oid			ancestor = lfirst_oid(lc);
+			List	   *ancestor_puboids = NIL;
+			List	   *ancestor_exceptpuboids = NIL;
 
-			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+			GetRelationPublications(ancestor, &ancestor_puboids,
+									&ancestor_exceptpuboids);
+
+			puboids = list_concat_unique_oid(puboids, ancestor_puboids);
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   ancestor_exceptpuboids);
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ad201af2f..5165e15cb17 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4634,9 +4634,48 @@ getPublications(Archive *fout)
 			(strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0);
 		pubinfo[i].pubgencols_type =
 			*(PQgetvalue(res, i, i_pubgencols));
+		pubinfo[i].excepttbls = (SimplePtrList)
+		{
+			NULL, NULL
+		};
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(pubinfo[i].dobj), fout);
+
+		if (fout->remoteVersion >= 190000)
+		{
+			int			ntbls;
+			PGresult   *res_tbls;
+
+			resetPQExpBuffer(query);
+			appendPQExpBuffer(query,
+							  "SELECT prrelid\n"
+							  "FROM pg_catalog.pg_publication_rel\n"
+							  "WHERE prpubid = %u and prexcept = true",
+							  pubinfo[i].dobj.catId.oid);
+
+			res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+			ntbls = PQntuples(res_tbls);
+			if (ntbls == 0)
+				continue;
+
+			for (int j = 0; j < ntbls; j++)
+			{
+				Oid			prrelid;
+				TableInfo  *tbinfo;
+
+				prrelid = atooid(PQgetvalue(res_tbls, j, 0));
+
+				tbinfo = findTableByOid(prrelid);
+				if (tbinfo == NULL)
+					continue;
+
+				simple_ptr_list_append(&pubinfo[i].excepttbls, tbinfo);
+			}
+
+			PQclear(res_tbls);
+		}
 	}
 
 cleanup:
@@ -4676,7 +4715,28 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		bool		first_tbl = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has EXCEPT TABLEs */
+		for (SimplePtrListCell *cell = pubinfo->excepttbls.head; cell; cell = cell->next)
+		{
+			TableInfo  *tbinfo = (TableInfo *) cell->ptr;
+
+			if (first_tbl)
+			{
+				appendPQExpBufferStr(query, " EXCEPT TABLE (");
+				first_tbl = false;
+			}
+			else
+				appendPQExpBufferStr(query, ", ");
+			appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+		}
+		if (!first_tbl)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4856,8 +4916,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
 		appendPQExpBufferStr(query,
-							 "SELECT tableoid, oid, prpubid, prrelid, "
+							 "SELECT tableoid, oid, prpubid, prrelid,\n"
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
 							 "(CASE\n"
 							 "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4868,6 +4929,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " WHERE prexcept = false");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..9eccd2ef495 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -676,6 +676,7 @@ typedef struct _PublicationInfo
 	bool		pubtruncate;
 	bool		pubviaroot;
 	PublishGencolsType pubgencols_type;
+	SimplePtrList excepttbls;
 } PublicationInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..a3fcf3c2b0a 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,36 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub10' => {
+		create_order => 92,
+		create_sql =>
+		  'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_inheritance_parent);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_inheritance_parent, ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..8cd73b3ad53 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -3134,6 +3151,35 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* Print publication the relation is excluded explicitly */
+		if (pset.sversion >= 190000)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT pubname\n"
+							  "FROM pg_catalog.pg_publication p\n"
+							  "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+							  "WHERE pr.prrelid = '%s'\n AND pr.prexcept\n"
+							  "ORDER BY 1;", oid);
+
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+				printTableAddFooter(&cont, _("Except Publications:"));
+
+			/* Might be an empty set - that's ok */
+			for (i = 0; i < tuples; i++)
+			{
+				printfPQExpBuffer(&buf, "    \"%s\"", PQgetvalue(result, i, 0));
+
+				printTableAddFooter(&cont, buf.data);
+			}
+			PQclear(result);
+		}
+
 		/*
 		 * If verbose, print NOT NULL constraints.
 		 */
@@ -6753,8 +6799,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6822,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b1ff6f6cd94..ab5c9991b51 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3637,7 +3637,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..e87dcd8aab6 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,14 +146,16 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern bool GetRelationPublications(Oid relid, List **pubids, List **except_pubids);
 
 /*---------
- * Expected values for pub_partopt parameter of GetPublicationRelations(),
+ * Expected values for pub_partopt parameter of
+ * GetPublicationIncludedRelations(), and GetPublicationExcludedRelations(),
  * which allows callers to specify which partitions of partitioned tables
  * mentioned in the publication they expect to see.
  *
@@ -168,9 +170,12 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationIncludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
+extern List *GetPublicationExcludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..ca1055051f9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4299,6 +4299,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4307,6 +4308,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4335,6 +4337,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_tables;	/* List of tables to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..31a3d6f471d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,13 +213,42 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
+SET client_min_messages = 'ERROR';
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+-- EXCEPT with asterisk: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
 RESET client_min_messages;
 \dRp+ testpub3
                                                       Publication testpub3
@@ -227,8 +256,8 @@ RESET client_min_messages;
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
-    "public.testpub_tbl3a"
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
 
 \dRp+ testpub4
                                                       Publication testpub4
@@ -236,10 +265,36 @@ Tables:
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
+    "public.testpub_tbl_parent"
+
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+\dRp+ testpub7
+                                                      Publication testpub7
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_parent"
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..6817422356d 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,39 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+RESET client_min_messages;
+
+\dRp+ testpub_foralltables_excepttable
+\dRp+ testpub_foralltables_excepttable1
+
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+-- EXCEPT with asterisk: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
 RESET client_min_messages;
 \dRp+ testpub3
 \dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
+\dRp+ testpub7
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..2a53aae7fbe
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,219 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT TABLE publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# ============================================
+# EXCEPT TABLE test cases for normal tables
+# ============================================
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Verify that data inserted to the excluded table is not replicated.
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+# ============================================
+# EXCEPT TABLE test cases for partitioned tables
+# Check behavior of EXCEPT TABLE with publish_via_partition_root on a
+# partitioned table and its partitions.
+# ============================================
+# Setup partitioned table and partitions on the publisher that map to normal
+# tables on the subscriber
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+	CREATE TABLE sch1.part2(a int);
+));
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = false
+# Excluding a partition while publish_via_partition_root = false prevents
+# replication of rows inserted into the partitioned table for that particular
+# partition.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on other partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table still allows rows inserted into the
+# partitioned table to be replicated via its partitions.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = true
+# When the partitioned table is excluded and publish_via_partition_root is true,
+# no rows from the table or its partitions are replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = true
+# When a partition is excluded but publish_via_partition_root is true,
+# rows published through the partitioned table can still be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1 ORDER BY a");
+is( $result, qq(1
+2
+6
+7), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on other partition');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#167shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#166)
Re: Skipping schema changes in publication

On Thu, Dec 18, 2025 at 5:15 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 18 Dec 2025 at 11:30, shveta malik <shveta.malik@gmail.com> wrote:

On Wed, Dec 17, 2025 at 11:24 AM shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Dec 16, 2025 at 2:50 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have also addressed the remaining comments and attached the latest patch.

Thanks. A few comments:

1)
+ if (!set_top && puballtables)
+ set_top = !list_member_oid(aexceptpubids, puboid);

In GetTopMostAncestorInPublication(), we have made the above change
which will now get ancestor from all-tables publication as well,
provided table is not part of 'except' List. Earlier this function was
only checking pg_subscription_rel and pg_publication_namespace which
does not include all-tables publication. Won't it change the
result-set for callers?

It can change the result set of callers. I analysed more and saw that
GetTopMostAncestorInPublication is called from 3 places.
1. pub_rf_contains_invalid_column: it is called when publication is
not ALL TABLES. It will have no impact with the change.
2. pub_contains_invalid_column : it is called for all type of
publication. it calls GetTopMostAncestorInPublication like:
```
if (pubviaroot && relation->rd_rel->relispartition)
{
publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors,
NULL, puballtables);

if (!OidIsValid(publish_as_relid))
publish_as_relid = relid;
}
```
In HEAD for ALL TABLES publication GetTopMostAncestorInPublication
will always return InvalidOid. With this patch it can have some value.
So there is a difference in behaviour.

3. get_rel_sync_entry
in HEAD we had
```
if (pub->alltables)
{
publish = true;
if (pub->pubviaroot && am_partition)
{
List *ancestors = get_partition_ancestors(relid);

pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
}
}
```
With patch this condition is not valid because we cannot set
'pub_relid = llast_oid(ancestors);' directly as the table can be
excluded.
So, the change in GetTopMostAncestorInPublication will even handle the
case of "ALL TABLES" publication.

Since we have a behaviour difference for the 2nd function, I have
removed the changes for 'ALL TABLES' from
GetTopMostAncestorInPublication and added it separately
'get_rel_sync_entry'. Thoughts?

I find the current implementation better, the previous one was
impacting the results of different paths.

Regarding:
+ if (list_member_oid(aexceptpubids, puboid))
+ {
+ list_free(aexceptpubids);
+ continue;
+ }

IMO, if puboid is part of apubids, that check is enough. This is
because aexceptpubids and apubids are mutually exclusive lists for a
particular 'ancestor'. But if we want to have it to avoid
schma-mapping check later, we should add a comment. How about:

This step isn’t strictly necessary, but we keep it so we can skip the
table if it appears in the EXCEPT list, avoiding an expensive
schema-mapping check later.

2)
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+ return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}

We can have an Assert here that pubid passed is not for ALL-Tables or
ALL-sequences

Added assert for all tables. I found during testing that this function
can be called for ALL SEQUENCES in HEAD. So I have not added an
assertion for it.
I think it is a bug and shared the same in [1]. Will add assert for
all sequences as well once the bug is fixed.

Okay.

3)
GetAllPublicationRelations:
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. This is not applicable to FOR ALL SEQUENCES
* publication.

+ * The list does not include relations that are explicitly excluded via the
+ * EXCEPT TABLE clause of the publication specified by pubid.

Suggestion:
/*
* If the publication publishes partition changes via their respective root
* partitioned tables, we must exclude partitions in favor of including the
* root partitioned tables. The list also excludes tables that are
* explicitly excluded via the EXCEPT TABLE clause of the publication
* identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
* publications.
*/

4)
GetAllPublicationRelations:
+ if (relkind == RELKIND_RELATION)
+ exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+ PUBLICATION_PART_ALL :
+ PUBLICATION_PART_ROOT);

Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));

Generally we keep such parameters' sanity checks as the first step. We
can add new code after Assert.

5)
ObjectsInAllPublicationToOids() only has one caller which calls it
under check: 'if (stmt->for_all_tables)'

Thus IMO, we do not need a switch-case in
ObjectsInAllPublicationToOids(). We can simply have a sanity check to
see it is 'PUBLICATION_ALL_TABLES' and then do the needed operation
for this pub-type.

6)
CreatePublication():
/*
* If publication is for ALL TABLES and relations is not empty, it means
* that there are some relations to be excluded from the publication.
* Else, relations is the list of relations to be added to the
* publication.
*/

Shall we rephrase slightly to:

/*
* If the publication is for ALL TABLES and 'relations' is not empty,
* it indicates that some relations should be excluded from the publication.
* Add those excluded relations to the publication with 'prexcept' set to true.
* Otherwise, 'relations' contains the list of relations to be explicitly
* included in the publication.
*/

7)
+ /* Associate objects with the publication. */
+ if (stmt->for_all_tables)
+ {
+ /* Invalidate relcache so that publication info is rebuilt. */
+ CacheInvalidateRelcacheAll();
+ }

I think this comment is misplaced. We shall have it at previous place, atop:
if (stmt->for_all_tables)
This is because here we are just trying to invalidate cache while at
previous place we are trying to associate.

Few more:

8)
get_rel_sync_entry()
+ List *exceptTablePubids = NIL;

At all other places, we are using exceptpubids, shall we use the same here?

9)
ObjectsInPublicationToOids()

case PUBLICATIONOBJ_TABLE:
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = (pubobj->pubobjtype ==
PUBLICATIONOBJ_EXCEPT_TABLE);
*rels = lappend(*rels, pubobj->pubtable);
break;

It looks slightly odd that for pubobjtype case
'PUBLICATIONOBJ_EXCEPT_TABLE', we have to check pubobjtype against
PUBLICATIONOBJ_EXCEPT_TABLE itself.

Shall we make it:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = true;
/* fall through */
case PUBLICATIONOBJ_TABLE:
*rels = lappend(*rels, pubobj->pubtable);
break;

We should also make pubobj->pubtable->except = false for PUBLICATIONOBJ_TABLE?

yes, right.

Updated the condition like:
case PUBLICATIONOBJ_EXCEPT_TABLE:
pubobj->pubtable->except = true;
*rels = lappend(*rels, pubobj->pubtable);
break;
case PUBLICATIONOBJ_TABLE:
pubobj->pubtable->except = false;
*rels = lappend(*rels, pubobj->pubtable);
break;

Looks good.

10)
I want to understand the usage of DO_PUBLICATION_EXCEPT_REL. Can you
give a scenario where its usage in DOTypeNameCompare() will be hit?
Its all other usages too need some analysis and validation.

In the current patch we are not setting an objecttype to
DO_PUBLICATION_EXCEPT_REL.
We are storing the list of except tables in 'pubinfo[i].excepttbls'
list in function getPublications and "pubinfo[i].dobj.objType =
DO_PUBLICATION". So, I don't see any requirement of
DO_PUBLICATION_EXCEPT_REL now. I have removed it.

Yes, that was my initial thought as well, that we might not need it.
But I’ll review it further and let you know.

11)
+ List *except_objects; /* List of publication object to be excluded */

object --> objects
Currently since we exclude only tables, does it make sense to name it
as except_tables?

I have also addressed the remaining comments and attached the updated v33 patch.
[1]: /messages/by-id/CALDaNm0qoNtsX+9KPug6qb=uC-H2iPMYW+gL=Hehx+NgOxga6w@mail.gmail.com

Thanks, will review.

thanks
Shveta

#168Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#166)
Re: Skipping schema changes in publication

Hi Shlok.

Some review comments for patch v33-0001 (code part)

======
src/backend/catalog/pg_publication.c

GetPublicationRelationsInternal:

1.
Static function names should be snake_case.

~~~

GetPublicationIncludedRelations:

2.
+/*
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR TABLE publication, this returns the list of relations explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+ Assert(!GetPublication(pubid)->alltables);
+
+ return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}

Why isn't the Assert also saying something about puballsequences, as
mentioned in the function comment?

~~~

GetAllPublicationRelations:

3.
+ * root partitioned tables. The list also excludes tables that are
+ * explicitly excluded via the EXCEPT TABLE clause of the publication
+ * identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
+ * publications.

3.
It seems wrong to say "FOR ALL SEQUENCES" ... that seems to assume the
"FOR ALL SEQUENCES" and "FOR ALL TABLES" cannot co-exist. Did you mean
"Neither of ... to published sequences"?

~

4.
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)

There are tricky rules about relation vs sequences and the
publish_via_partition_root parameter value. It would be better if you
encapsulate all this within this function. Specifically, it would be
simpler if you passed the 'Publication' arg instead of the pubid. Then
you can get the pubviaroot value from that (within the function)
instead of passing around "fake" values of false when you are looking
at RELKIND_SEQUENCE.
======
src/backend/commands/publicationcmds.c

ObjectsInAllPublicationToOids:

5.
+ foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+ {
+ if (puballobj->pubobjtype != PUBLICATION_ALL_TABLES)
+ continue;
+
+ foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_tables)
+ {
+ pubobj->pubtable->except = true;
+ *rels = lappend(*rels, pubobj->pubtable);
+ }
+ }

I think it's tidier to code this like below:

if (puballobj->pubobjtype == PUBLICATION_ALL_TABLES)
{
foreach_ptr...
}

~~~

pub_contains_invalid_column:

6.
 bool
 pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
  bool pubviaroot, char pubgencols_type,
- bool *invalid_column_list,
- bool *invalid_gen_col)
+ bool *invalid_column_list, bool *invalid_gen_col)

Why does this change even exist at all in this patch?

~~~

CreatePublication:

7.
+ /*
+ * If the publication is for ALL TABLES and 'relations' is not empty, it
+ * indicates that some relations should be excluded from the publication.
+ * Add those excluded relations to the publication with 'prexcept' set to
+ * true. Otherwise, 'relations' contains the list of relations to be
+ * explicitly included in the publication.
+ */
+ if (relations != NIL)
+ {
+ List    *rels;
+
+ rels = OpenTableList(relations);
+ TransformPubWhereClauses(rels, pstate->p_sourcetext,
+ publish_via_partition_root);
+
+ CheckPubRelationColumnList(stmt->pubname, rels,
+    schemaidlist != NIL,
+    publish_via_partition_root);
+
+ PublicationAddTables(puboid, rels, true, NULL);
+ CloseTableList(rels);
+ }
+

The comment and the code don't match. The comment is talking about
rules for FOR ALL TABLES, but puballtables is not part of any
condition here (??). Was all this supposed to be within the "if
(stmt->for_all_tables)" code block?

======
src/bin/pg_dump/pg_dump.c

8.
- "SELECT tableoid, oid, prpubid, prrelid, "
+ "SELECT tableoid, oid, prpubid, prrelid,\n"
  "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
  "(CASE\n"
  "  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4868,6 +4929,9 @@ getPublicationTables(Archive *fout, TableInfo
tblinfo[], int numTables)
  "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
  "  ELSE NULL END) prattrs "
  "FROM pg_catalog.pg_publication_rel pr");
+ if (fout->remoteVersion >= 190000)
+ appendPQExpBufferStr(query, " WHERE prexcept = false");

8a
Isn't it better to qualify everything here with the alias 'pr'?

~

8b.
Also "WHERE NOT pr.prexcept;" might be more conssitent with other code
I saw in describe.c

======
src/bin/pg_dump/pg_dump.h

9.
PublishGencolsType pubgencols_type;
+ SimplePtrList excepttbls;
} PublicationInfo;

How about "tables instead of "tbls" (e.g. "excepttables" or
"except_tables") here? That would also be more consistent with the
other puballtables member.

======
src/test/regress/sql/publication.sql

10.
RESET client_min_messages;
\dRp+ testpub3
\dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
+\dRp+ testpub7

I feel it would be better to keep each \dRp+ together with the test it
belongs to, rather than have a bunch of different tests which are then
followed by a bunch of different \dRp+. Note: this same comment
applies to other place of places -- not just here. Check everywhere
you do \dRp+

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

#169Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Peter Smith (#168)
1 attachment(s)
Re: Skipping schema changes in publication

On Mon, 22 Dec 2025 at 11:37, Peter Smith <smithpb2250@gmail.com> wrote:

Hi Shlok.

Some review comments for patch v33-0001 (code part)

======
src/backend/catalog/pg_publication.c

GetPublicationRelationsInternal:

1.
Static function names should be snake_case.

~~~

GetPublicationIncludedRelations:

2.
+/*
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR TABLE publication, this returns the list of relations explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES or FOR ALL SEQUENCES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+ Assert(!GetPublication(pubid)->alltables);
+
+ return GetPublicationRelationsInternal(pubid, pub_partopt, false);
+}

Why isn't the Assert also saying something about puballsequences, as
mentioned in the function comment?

I reported a similar kind of issue in HEAD in [1]/messages/by-id/CAA4eK1+rnjBOvkiQC2r4LuTwuje653iVPPAXcmJZXPpKvsNbOQ@mail.gmail.com.
As per the latest discussion, I understood that it is ok to call this
function for ALL SEQUENCES.
I have updated the comment.

~~~

GetAllPublicationRelations:

3.
+ * root partitioned tables. The list also excludes tables that are
+ * explicitly excluded via the EXCEPT TABLE clause of the publication
+ * identified by pubid. Neither of these rules applies to FOR ALL SEQUENCES
+ * publications.

3.
It seems wrong to say "FOR ALL SEQUENCES" ... that seems to assume the
"FOR ALL SEQUENCES" and "FOR ALL TABLES" cannot co-exist. Did you mean
"Neither of ... to published sequences"?

I have modified the comment.

~

4.
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)

There are tricky rules about relation vs sequences and the
publish_via_partition_root parameter value. It would be better if you
encapsulate all this within this function. Specifically, it would be
simpler if you passed the 'Publication' arg instead of the pubid. Then
you can get the pubviaroot value from that (within the function)
instead of passing around "fake" values of false when you are looking
at RELKIND_SEQUENCE.
======
src/backend/commands/publicationcmds.c

ObjectsInAllPublicationToOids:

5.
+ foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+ {
+ if (puballobj->pubobjtype != PUBLICATION_ALL_TABLES)
+ continue;
+
+ foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_tables)
+ {
+ pubobj->pubtable->except = true;
+ *rels = lappend(*rels, pubobj->pubtable);
+ }
+ }

I think it's tidier to code this like below:

if (puballobj->pubobjtype == PUBLICATION_ALL_TABLES)
{
foreach_ptr...
}

~~~

pub_contains_invalid_column:

6.
bool
pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
bool pubviaroot, char pubgencols_type,
- bool *invalid_column_list,
- bool *invalid_gen_col)
+ bool *invalid_column_list, bool *invalid_gen_col)

Why does this change even exist at all in this patch?

This change is not required. I have reverted it.

~~~

CreatePublication:

7.
+ /*
+ * If the publication is for ALL TABLES and 'relations' is not empty, it
+ * indicates that some relations should be excluded from the publication.
+ * Add those excluded relations to the publication with 'prexcept' set to
+ * true. Otherwise, 'relations' contains the list of relations to be
+ * explicitly included in the publication.
+ */
+ if (relations != NIL)
+ {
+ List    *rels;
+
+ rels = OpenTableList(relations);
+ TransformPubWhereClauses(rels, pstate->p_sourcetext,
+ publish_via_partition_root);
+
+ CheckPubRelationColumnList(stmt->pubname, rels,
+    schemaidlist != NIL,
+    publish_via_partition_root);
+
+ PublicationAddTables(puboid, rels, true, NULL);
+ CloseTableList(rels);
+ }
+

The comment and the code don't match. The comment is talking about
rules for FOR ALL TABLES, but puballtables is not part of any
condition here (??). Was all this supposed to be within the "if
(stmt->for_all_tables)" code block?

For both ALL TABLES publication and non-ALL TABLES publication we need
the same code block.
Setting of prexcept flag will be handled in PublicationAddTables.
This comment clarifies what the list 'relations' would mean for ALL
TABLES publication and non-ALL TABLES publication

======
src/bin/pg_dump/pg_dump.c

8.
- "SELECT tableoid, oid, prpubid, prrelid, "
+ "SELECT tableoid, oid, prpubid, prrelid,\n"
"pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
"(CASE\n"
"  WHEN pr.prattrs IS NOT NULL THEN\n"
@@ -4868,6 +4929,9 @@ getPublicationTables(Archive *fout, TableInfo
tblinfo[], int numTables)
"      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
"  ELSE NULL END) prattrs "
"FROM pg_catalog.pg_publication_rel pr");
+ if (fout->remoteVersion >= 190000)
+ appendPQExpBufferStr(query, " WHERE prexcept = false");

8a
Isn't it better to qualify everything here with the alias 'pr'?

It is an existing code. So I prefer not to modify it in this patch. I
have added the alias for the column added by this patch.

~

8b.
Also "WHERE NOT pr.prexcept;" might be more conssitent with other code
I saw in describe.c

======
src/bin/pg_dump/pg_dump.h

9.
PublishGencolsType pubgencols_type;
+ SimplePtrList excepttbls;
} PublicationInfo;

How about "tables instead of "tbls" (e.g. "excepttables" or
"except_tables") here? That would also be more consistent with the
other puballtables member.

======
src/test/regress/sql/publication.sql

10.
RESET client_min_messages;
\dRp+ testpub3
\dRp+ testpub4
+\dRp+ testpub5
+\dRp+ testpub6
+\dRp+ testpub7

I feel it would be better to keep each \dRp+ together with the test it
belongs to, rather than have a bunch of different tests which are then
followed by a bunch of different \dRp+. Note: this same comment
applies to other place of places -- not just here. Check everywhere
you do \dRp+

I have addressed the remaining comments, did some cosmetic changes and
addressed the comment shared by Shveta in [2]/messages/by-id/CAJpy0uCf5tXvqyVS3GQzU9J5HdSLAxX6Lxt1UKY4HJ8qnimCAw@mail.gmail.com.
[1]: /messages/by-id/CAA4eK1+rnjBOvkiQC2r4LuTwuje653iVPPAXcmJZXPpKvsNbOQ@mail.gmail.com
[2]: /messages/by-id/CAJpy0uCf5tXvqyVS3GQzU9J5HdSLAxX6Lxt1UKY4HJ8qnimCAw@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v34-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v34-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 57cb41080989077653ed3185912acedbe8171c96 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Dec 2025 22:41:23 +0530
Subject: [PATCH v34] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE PUBLICATION allows one or more
tables to be excluded. The publisher will not send the data of excluded
tables to the subscriber.

The new syntax allows specifying excluded relations when creating a
publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/logical-replication.sgml         |   6 +-
 doc/src/sgml/ref/create_publication.sgml      |  55 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |   8 +-
 src/backend/catalog/pg_publication.c          | 156 ++++++++++---
 src/backend/commands/publicationcmds.c        | 106 ++++++---
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  33 ++-
 src/backend/replication/pgoutput/pgoutput.c   |  22 +-
 src/backend/utils/cache/relcache.c            |  21 +-
 src/bin/pg_dump/pg_dump.c                     |  64 +++++
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 +++
 src/bin/psql/describe.c                       |  87 ++++++-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_publication.h          |  13 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  77 +++++-
 src/test/regress/sql/publication.sql          |  34 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 219 ++++++++++++++++++
 22 files changed, 851 insertions(+), 112 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..9e847152b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation is excluded from the publication. See
+       <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index b3faaa675ef..ba454b78cbc 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -116,7 +116,11 @@
    <literal>FOR TABLES IN SCHEMA</literal>, <literal>FOR ALL TABLES</literal>,
    or <literal>FOR ALL SEQUENCES</literal>. Unlike tables, sequences can be
    synchronized at any time. For more information, see
-   <xref linkend="logical-replication-sequences"/>.
+   <xref linkend="logical-replication-sequences"/>. When a publication is
+   created with <literal>FOR ALL TABLES</literal>, tables can be explicitly
+   excluded from publication using the
+   <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>
+   clause.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..77bca21ce92 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">except_table_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>and <replaceable class="parameter">except_table_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,8 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. Tables listed in
+      EXCEPT TABLE are excluded from the publication.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +189,35 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication.
+     </para>
+     <para>
+      For inherited tables, if <literal>ONLY</literal> is specified before the
+      table name, only that table is excluded from the publication. If
+      <literal>ONLY</literal> is not specified, the table and all its descendant
+      tables (if any) are excluded. Optionally, <literal>*</literal> can be
+      specified after the table name to explicitly indicate that descendant
+      tables are excluded. This does not apply to a partitioned table, however.
+     </para>
+     <para>
+      For partitioned tables, when <literal>publish_via_partition_root</literal>
+      is set to <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a root partitioned table has no
+      effect, as changes are replicated via the leaf partitions. Specifying a
+      leaf partition excludes only that partition from replication.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -487,6 +521,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES;
    all sequences for synchronization:
 <programlisting>
 CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all sequences for synchronization, and
+   all changes in all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT (users, departments);
 </programlisting>
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..c8fcb126c8b 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1301,7 +1301,8 @@ SELECT $1 \parse stmt1
         special attributes such as <literal>NOT NULL</literal> or defaults.
         Associated indexes, constraints, rules, and triggers are
         also shown.  For foreign tables, the associated foreign
-        server is shown as well.
+        server is shown as well. For a table, the associated publications and
+        the publications from which the table is excluded are also shown.
         (<quote>Matching the pattern</quote> is defined in
         <xref linkend="app-psql-patterns"/> below.)
         </para>
@@ -2103,8 +2104,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 7aa3f179924..c92ff928878 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -366,11 +366,25 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = NIL;
+		List	   *aexceptpubids = NIL;
 		List	   *aschemaPubids = NIL;
 
+		GetRelationPublications(ancestor, &apubids, &aexceptpubids);
+
 		level++;
 
+		/*
+		 * This step is not strictly necessary, but is kept to allow skipping
+		 * the ancestor if it is part of the publication's EXCEPT TABLE list,
+		 * avoiding an expensive schema-mapping check later.
+		 */
+		if (list_member_oid(aexceptpubids, puboid))
+		{
+			list_free(aexceptpubids);
+			continue;
+		}
+
 		if (list_member_oid(apubids, puboid))
 		{
 			topmost_relid = ancestor;
@@ -391,6 +405,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 		}
 
 		list_free(apubids);
+		list_free(aexceptpubids);
 		list_free(aschemaPubids);
 	}
 
@@ -466,6 +481,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Handle the case where a partition is excluded by EXCEPT TABLE while
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relispartition)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
+	/*
+	 * Handle the case where a partitioned table is excluded by EXCEPT TABLE
+	 * while publish_via_partition_root = false.
+	 */
+	if (pub->alltables && !pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		ereport(WARNING,
+				(errmsg("partitioned table \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "false")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +517,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,38 +786,58 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
-List *
-GetRelationPublications(Oid relid)
+/*
+ * Get the list of publication oids associated with a specified relation.
+ *
+ * Parameter 'pubids' returns the OIDs of the publications the relation is part
+ * of. Parameter 'except_pubids' returns the OIDs of publications the relation
+ * is excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */
+bool
+GetRelationPublications(Oid relid, List **pubids, List **except_pubids)
 {
-	List	   *result = NIL;
 	CatCList   *pubrellist;
-	int			i;
+	bool		found = false;
 
 	/* Find all publications associated with the relation. */
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
-		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		Oid			pubid = pubrel->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (pubrel->prexcept)
+		{
+			if (except_pubids)
+				*except_pubids = lappend_oid(*except_pubids, pubid);
+		}
+		else
+		{
+			if (pubids)
+				*pubids = lappend_oid(*pubids, pubid);
+			found = true;
+		}
 	}
 
 	ReleaseSysCacheList(pubrellist);
 
-	return result;
+	return found;
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Internal function to get the list of relation OIDs for a publication.
  *
- * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * If except_flag is true, returns the list of relations excluded from the
+ * publication; otherwise, returns the list of relations included in the
+ * publication.
  */
-List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+static List *
+get_publication_relations_internal(Oid pubid, PublicationPartOpt pub_partopt,
+								   bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +862,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
 	}
 
 	systable_endscan(scan);
@@ -819,6 +878,36 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 	return result;
 }
 
+/*
+ * Return the list of relation OIDs for a publication.
+ *
+ * For a FOR TABLE publication, this returns the list of relations explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(!GetPublication(pubid)->alltables);
+
+	return get_publication_relations_internal(pubid, pub_partopt, false);
+}
+
+/*
+ * Return the list of relation OIDs excluded from a publication.
+ * This is only applicable for FOR ALL TABLES publications.
+ */
+List *
+GetPublicationExcludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(GetPublication(pubid)->alltables);
+
+	return get_publication_relations_internal(pubid, pub_partopt, true);
+}
+
 /*
  * Gets list of publication oids for publications marked as FOR ALL TABLES.
  */
@@ -864,18 +953,29 @@ GetAllTablesPublications(void)
  * partitioned tables, we must exclude partitions in favor of including the
  * root partitioned tables. This is not applicable to FOR ALL SEQUENCES
  * publication.
+ *
+ * FOR ALL TABLES publication, the list excludes the tables that are explicitly
+ * mentioned in EXCEPT TABLE clause.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Publication *pub, char relkind)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist = NIL;
+	bool		pubviaroot = pub->pubviaroot;
+	Oid			pubid = pub->oid;
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
+	if (relkind == RELKIND_RELATION)
+		exceptlist = GetPublicationExcludedRelations(pubid, pubviaroot ?
+													 PUBLICATION_PART_ALL :
+													 PUBLICATION_PART_ROOT);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -891,7 +991,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +1013,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,17 +1270,17 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+				pub_elem_tables = GetAllPublicationRelations(pub_elem,
+															 RELKIND_RELATION);
 			else
 			{
 				List	   *relids,
 						   *schemarelids;
 
-				relids = GetPublicationRelations(pub_elem->oid,
-												 pub_elem->pubviaroot ?
-												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+				relids = GetPublicationIncludedRelations(pub_elem->oid,
+														 pub_elem->pubviaroot ?
+														 PUBLICATION_PART_ROOT :
+														 PUBLICATION_PART_LEAF);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1469,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication, RELKIND_SEQUENCE);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a1983508950..84a3d95ea8a 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -170,6 +170,30 @@ parse_publication_options(ParseState *pstate,
 	}
 }
 
+/*
+ * Convert the PublicationObjSpec list which is part of
+ * PublicationAllObjSpecType list into PublicationTable list.
+ */
+static void
+ObjectsInAllPublicationToOids(List *puballobjspec_list,
+							  ParseState *pstate, List **rels)
+{
+	if (!puballobjspec_list)
+		return;
+
+	foreach_ptr(PublicationAllObjSpec, puballobj, puballobjspec_list)
+	{
+		if (puballobj->pubobjtype == PUBLICATION_ALL_TABLES)
+		{
+			foreach_ptr(PublicationObjSpec, pubobj, puballobj->except_tables)
+			{
+				pubobj->pubtable->except = true;
+				*rels = lappend(*rels, pubobj->pubtable);
+			}
+		}
+	}
+}
+
 /*
  * Convert the PublicationObjSpecType list into schema oid list and
  * PublicationTable list.
@@ -193,7 +217,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 
 		switch (pubobj->pubobjtype)
 		{
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -514,8 +543,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * a target. However, WAL records for TRUNCATE specify both a root and
 		 * its leaves.
 		 */
-		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+		relids = GetPublicationIncludedRelations(pubid,
+												 PUBLICATION_PART_ALL);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -925,14 +954,8 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 
 	/* Associate objects with the publication. */
 	if (stmt->for_all_tables)
-	{
-		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
-		 */
-		CacheInvalidateRelcacheAll();
-	}
+		ObjectsInAllPublicationToOids(stmt->pubobjects, pstate, &relations);
+
 	else if (!stmt->for_all_sequences)
 	{
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
@@ -944,22 +967,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
 
-		if (relations != NIL)
-		{
-			List	   *rels;
-
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
-
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
-
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
-
 		if (schemaidlist != NIL)
 		{
 			/*
@@ -971,8 +978,37 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 		}
 	}
 
+	/*
+	 * If the publication is for ALL TABLES and 'relations' is not empty, it
+	 * indicates that some relations should be excluded from the publication.
+	 * Add those excluded relations to the publication with 'prexcept' set to
+	 * true. Otherwise, 'relations' contains the list of relations to be
+	 * explicitly included in the publication.
+	 */
+	if (relations != NIL)
+	{
+		List	   *rels;
+
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
+
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
+
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
+
 	table_close(rel, RowExclusiveLock);
 
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	if (wal_level != WAL_LEVEL_LOGICAL)
@@ -1040,8 +1076,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
 						   AccessShareLock);
 
-		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+		root_relids = GetPublicationIncludedRelations(pubform->oid,
+													  PUBLICATION_PART_ROOT);
 
 		foreach(lc, root_relids)
 		{
@@ -1160,8 +1196,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 * trees, not just those explicitly mentioned in the publication.
 		 */
 		if (root_relids == NIL)
-			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+			relids = GetPublicationIncludedRelations(pubform->oid,
+													 PUBLICATION_PART_ALL);
 		else
 		{
 			/*
@@ -1246,8 +1282,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		PublicationDropTables(pubid, rels, false);
 	else						/* AP_SetObjects */
 	{
-		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+		List	   *oldrelids = GetPublicationIncludedRelations(pubid,
+																PUBLICATION_PART_ROOT);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1348,6 +1384,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc_object(PublicationRelInfo);
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1398,7 +1435,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationIncludedRelations(pubform->oid,
+												  PUBLICATION_PART_ROOT);
 
 		foreach(lc, reloids)
 		{
@@ -1761,6 +1799,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1833,6 +1872,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..3ea95ae1a26 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8687,7 +8687,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18882,7 +18882,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..bcf0bc57aa4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -455,6 +455,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				pub_except_obj_list opt_pub_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -592,6 +593,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 <publicationobjectspec> PublicationExceptObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10787,7 +10789,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * pub_all_obj_type is one of:
  *
- *		TABLES
+ *		TABLES [EXCEPT [TABLE] ( table [, ...] )]
  *		SEQUENCES
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
@@ -10813,6 +10815,7 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
+					n->pubobjects = $5;
 					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
@@ -10853,6 +10856,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10928,11 +10932,19 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_pub_except_clause:
+			EXCEPT opt_table '(' pub_except_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_pub_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_tables = $3;
+						if($$->except_tables != NULL)
+							preprocess_pubobj_list($$->except_tables, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10949,6 +10961,23 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+PublicationExceptObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+pub_except_obj_list: PublicationExceptObjSpec
+					{ $$ = list_make1($1); }
+			| pub_except_obj_list ',' PublicationExceptObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..88174bd299a 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2084,7 +2084,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = NIL;
+		List	   *exceptpubids = NIL;
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2099,6 +2100,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		char		relkind = get_rel_relkind(relid);
 		List	   *rel_publications = NIL;
 
+		GetRelationPublications(relid, &pubids, &exceptpubids);
+
 		/* Reload publications if needed before use. */
 		if (!publications_valid)
 		{
@@ -2205,9 +2208,21 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				if (pub->pubviaroot && am_partition)
 				{
 					List	   *ancestors = get_partition_ancestors(relid);
+					int			level = 0;
+
+					foreach_oid(ancestor, ancestors)
+					{
+						List	   *aexceptpubids = NIL;
 
-					pub_relid = llast_oid(ancestors);
-					ancestor_level = list_length(ancestors);
+						level++;
+						GetRelationPublications(ancestor, NULL, &aexceptpubids);
+
+						if (!list_member_oid(aexceptpubids, pub->oid))
+						{
+							pub_relid = ancestor;
+							ancestor_level = level;
+						}
+					}
 				}
 			}
 
@@ -2322,6 +2337,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 
 		list_free(pubids);
 		list_free(schemaPubids);
+		list_free(exceptpubids);
 		list_free(rel_publications);
 
 		entry->replicate_valid = true;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2d0cb7bcfd4..bc5f9495923 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5793,7 +5793,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
-	List	   *puboids;
+	List	   *puboids = NIL;
+	List	   *exceptpuboids = NIL;
+	List	   *alltablespuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5831,7 +5833,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	GetRelationPublications(relid, &puboids, &exceptpuboids);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5843,16 +5845,25 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		foreach(lc, ancestors)
 		{
 			Oid			ancestor = lfirst_oid(lc);
+			List	   *ancestor_puboids = NIL;
+			List	   *ancestor_exceptpuboids = NIL;
 
-			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+			GetRelationPublications(ancestor, &ancestor_puboids,
+									&ancestor_exceptpuboids);
+
+			puboids = list_concat_unique_oid(puboids, ancestor_puboids);
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   ancestor_exceptpuboids);
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 27f6be3f0f8..0021a48a7b6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4634,9 +4634,48 @@ getPublications(Archive *fout)
 			(strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0);
 		pubinfo[i].pubgencols_type =
 			*(PQgetvalue(res, i, i_pubgencols));
+		pubinfo[i].except_tables = (SimplePtrList)
+		{
+			NULL, NULL
+		};
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(pubinfo[i].dobj), fout);
+
+		if (fout->remoteVersion >= 190000)
+		{
+			int			ntbls;
+			PGresult   *res_tbls;
+
+			resetPQExpBuffer(query);
+			appendPQExpBuffer(query,
+							  "SELECT prrelid\n"
+							  "FROM pg_catalog.pg_publication_rel\n"
+							  "WHERE prpubid = %u and prexcept = true",
+							  pubinfo[i].dobj.catId.oid);
+
+			res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+			ntbls = PQntuples(res_tbls);
+			if (ntbls == 0)
+				continue;
+
+			for (int j = 0; j < ntbls; j++)
+			{
+				Oid			prrelid;
+				TableInfo  *tbinfo;
+
+				prrelid = atooid(PQgetvalue(res_tbls, j, 0));
+
+				tbinfo = findTableByOid(prrelid);
+				if (tbinfo == NULL)
+					continue;
+
+				simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo);
+			}
+
+			PQclear(res_tbls);
+		}
 	}
 
 cleanup:
@@ -4676,7 +4715,28 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		bool		first_tbl = true;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include exception tables if the publication has EXCEPT TABLEs */
+		for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell; cell = cell->next)
+		{
+			TableInfo  *tbinfo = (TableInfo *) cell->ptr;
+
+			if (first_tbl)
+			{
+				appendPQExpBufferStr(query, " EXCEPT TABLE (");
+				first_tbl = false;
+			}
+			else
+				appendPQExpBufferStr(query, ", ");
+			appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+		}
+		if (!first_tbl)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4856,6 +4916,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
@@ -4868,6 +4929,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " WHERE NOT pr.prexcept");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..a19e77d149e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -676,6 +676,7 @@ typedef struct _PublicationInfo
 	bool		pubtruncate;
 	bool		pubviaroot;
 	PublishGencolsType pubgencols_type;
+	SimplePtrList except_tables;
 } PublicationInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..a3fcf3c2b0a 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,36 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub10' => {
+		create_order => 92,
+		create_sql =>
+		  'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_inheritance_parent);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_inheritance_parent, ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..8cd73b3ad53 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -3134,6 +3151,35 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* Print publication the relation is excluded explicitly */
+		if (pset.sversion >= 190000)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT pubname\n"
+							  "FROM pg_catalog.pg_publication p\n"
+							  "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+							  "WHERE pr.prrelid = '%s'\n AND pr.prexcept\n"
+							  "ORDER BY 1;", oid);
+
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+				printTableAddFooter(&cont, _("Except Publications:"));
+
+			/* Might be an empty set - that's ok */
+			for (i = 0; i < tuples; i++)
+			{
+				printfPQExpBuffer(&buf, "    \"%s\"", PQgetvalue(result, i, 0));
+
+				printTableAddFooter(&cont, buf.data);
+			}
+			PQclear(result);
+		}
+
 		/*
 		 * If verbose, print NOT NULL constraints.
 		 */
@@ -6753,8 +6799,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6822,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index ab2712216b5..97c34379556 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3657,7 +3657,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..bd094113c7a 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,14 +146,16 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern bool GetRelationPublications(Oid relid, List **pubids, List **except_pubids);
 
 /*---------
- * Expected values for pub_partopt parameter of GetPublicationRelations(),
+ * Expected values for pub_partopt parameter of
+ * GetPublicationIncludedRelations(), and GetPublicationExcludedRelations(),
  * which allows callers to specify which partitions of partitioned tables
  * mentioned in the publication they expect to see.
  *
@@ -168,9 +170,12 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationIncludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
+extern List *GetPublicationExcludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Publication *pub, char relkind);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 92cc36dfdf6..e7d7f3ba85c 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..ca1055051f9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4299,6 +4299,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4307,6 +4308,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4335,6 +4337,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_tables;	/* List of tables to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..30073f59706 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,33 +213,88 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
-DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
 RESET client_min_messages;
+DROP TABLE testpub_tbl2;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
 \dRp+ testpub3
                                                       Publication testpub3
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
-    "public.testpub_tbl3a"
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
 
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
 \dRp+ testpub4
                                                       Publication testpub4
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
+    "public.testpub_tbl_parent"
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+-- EXCEPT with asterisk: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
+\dRp+ testpub7
+                                                      Publication testpub7
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_parent"
+
+RESET client_min_messages;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..920da5360ad 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,38 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+\dRp+ testpub_foralltables_excepttable
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+\dRp+ testpub_foralltables_excepttable1
+
+RESET client_min_messages;
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
-RESET client_min_messages;
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
 \dRp+ testpub3
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
 \dRp+ testpub4
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+\dRp+ testpub5
+-- EXCEPT with asterisk: exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+\dRp+ testpub6
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
+\dRp+ testpub7
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+RESET client_min_messages;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 85d10a89994..b8e5c54c314 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..2a53aae7fbe
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,219 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT TABLE publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# ============================================
+# EXCEPT TABLE test cases for normal tables
+# ============================================
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+# Verify that data inserted to the excluded table is not replicated.
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+# ============================================
+# EXCEPT TABLE test cases for partitioned tables
+# Check behavior of EXCEPT TABLE with publish_via_partition_root on a
+# partitioned table and its partitions.
+# ============================================
+# Setup partitioned table and partitions on the publisher that map to normal
+# tables on the subscriber
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+	CREATE TABLE sch1.part2(a int);
+));
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = false
+# Excluding a partition while publish_via_partition_root = false prevents
+# replication of rows inserted into the partitioned table for that particular
+# partition.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on other partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table still allows rows inserted into the
+# partitioned table to be replicated via its partitions.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = true
+# When the partitioned table is excluded and publish_via_partition_root is true,
+# no rows from the table or its partitions are replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = true
+# When a partition is excluded but publish_via_partition_root is true,
+# rows published through the partitioned table can still be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1 ORDER BY a");
+is( $result, qq(1
+2
+6
+7), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on other partition');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#170Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#169)
Re: Skipping schema changes in publication

Hi Shlok

Some review comments for patch v34-0001 (code)

======
src/backend/catalog/pg_publication.c

1.
+static List *
+get_publication_relations_internal(Oid pubid, PublicationPartOpt pub_partopt,
+    bool except_flag)

No need to name this function as "_internal"; the snake_case name and
static already indicate it is internal.

======
src/bin/pg_dump/pg_dump.c

getPublications:

2.
+ if (fout->remoteVersion >= 190000)
+ {
+ int ntbls;
+ PGresult   *res_tbls;
+
+ resetPQExpBuffer(query);
+ appendPQExpBuffer(query,
+   "SELECT prrelid\n"
+   "FROM pg_catalog.pg_publication_rel\n"
+   "WHERE prpubid = %u and prexcept = true",
+   pubinfo[i].dobj.catId.oid);
+
+ res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntbls = PQntuples(res_tbls);
+ if (ntbls == 0)
+ continue;
+
+ for (int j = 0; j < ntbls; j++)
+ {
+ Oid prrelid;
+ TableInfo  *tbinfo;
+
+ prrelid = atooid(PQgetvalue(res_tbls, j, 0));
+
+ tbinfo = findTableByOid(prrelid);
+ if (tbinfo == NULL)
+ continue;
+
+ simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo);
+ }
+
+ PQclear(res_tbls);
+ }

2a.
I suppose this code is for populating the list of all tables except
those excluded, but there is no explanatory comment stating the
purpose of all this.

~

2b.
BEFORE
"WHERE prpubid = %u and prexcept = true"

SUGGESTION
"WHERE prpubid = %u AND prexcept"

~~~

dumpPublication:

3.
+ {
+ bool first_tbl = true;
+
  appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+ /* Include exception tables if the publication has EXCEPT TABLEs */
+ for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell;
cell = cell->next)
+ {
+ TableInfo  *tbinfo = (TableInfo *) cell->ptr;
+
+ if (first_tbl)
+ {
+ appendPQExpBufferStr(query, " EXCEPT TABLE (");
+ first_tbl = false;
+ }
+ else
+ appendPQExpBufferStr(query, ", ");
+ appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+ }
+ if (!first_tbl)
+ appendPQExpBufferStr(query, ")");
+ }

3a.
That code comment seems backwards.

BEFORE
/* Include exception tables if the publication has EXCEPT TABLEs */

SUGGESTION
/* Include EXCEPT TABLE clause if there are except_tables. */

~~~

3b.
Although it works OK, I felt the following looked strange:
+ if (!first_tbl)
+ appendPQExpBufferStr(query, ")");

IMO it would be better implemented as a counter:

Replace
bool first_tbl = true;
with
int n_excluded = 0;

Then,
+ if (first_tbl)
+ {
+ appendPQExpBufferStr(query, " EXCEPT TABLE (");
+ first_tbl = false;
+ }
becomes
+ if (++n_excluded == 1)
+ appendPQExpBufferStr(query, " EXCEPT TABLE (");
And,
+ if (!first_tbl)
+ appendPQExpBufferStr(query, ")");
becomes
+ if (n_excluded > 0)
+ appendPQExpBufferStr(query, ")");

======
src/bin/psql/describe.c

describeOneTableDetails:

4.
+ /* Print publication the relation is excluded explicitly */
+ if (pset.sversion >= 190000)

The comment doesn't seem right:

SUGGESTION
Print publications that the table is explicitly excluded from

======
src/test/regress/sql/publication.sql

5.
Missing tests.

There are no test cases to show that \d is working for printing the
"Except Publications:".

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

#171shveta malik
shveta.malik@gmail.com
In reply to: Shlok Kyal (#169)
Re: Skipping schema changes in publication

On Tue, Dec 23, 2025 at 12:03 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have addressed the remaining comments, did some cosmetic changes and
addressed the comment shared by Shveta in [2].
[1]: /messages/by-id/CAA4eK1+rnjBOvkiQC2r4LuTwuje653iVPPAXcmJZXPpKvsNbOQ@mail.gmail.com
[2]: /messages/by-id/CAJpy0uCf5tXvqyVS3GQzU9J5HdSLAxX6Lxt1UKY4HJ8qnimCAw@mail.gmail.com

Thank You for the patch. Please find a few comments:

1)
GetTopMostAncestorInPublication():

+ if (list_member_oid(aexceptpubids, puboid))
+ {
+ list_free(aexceptpubids);
+ continue;
+ }

We need to do 'list_free(apubids)' as well here.

2)
GetTopMostAncestorInPublication(). Currently it has:

if (list_member_oid(aexceptpubids, puboid))
...
if (list_member_oid(apubids, puboid))
...
else
...schema mapping check

IMO more natural order of checks will be

if (list_member_oid(apubids, puboid))
..
else if (list_member_oid(aexceptpubids, puboid))
...
else
...schema mapping check

3)
+/*
+ * Return the list of relation OIDs excluded from a publication.
+ * This is only applicable for FOR ALL TABLES publications.
+ */
+List *
+GetPublicationExcludedRelations(Oid pubid, PublicationPartOpt pub_partopt)

a) Since now 'Relations' term means both tables and sequences, but
here we mean only Tables, we can rename it to have 'Tables' rather
than 'Relations'

b) Similar to GetAllPublicationRelations which is for 'ALL Tables'
pub, we can rename it to have 'All'

So the name can be 'GetAllPublicationExcludedTables' to be more clear.

Also we can move this function close to GetAllPublicationRelations as
it is more related to that.

4)
ObjectsInPublicationToOids()
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = true;
+ *rels = lappend(*rels, pubobj->pubtable);
+ break;

Let me know when this will be hit when we already have
'ObjectsInAllPublicationToOids' in place?

5)
get_rel_sync_entry():
+ level++;
+ GetRelationPublications(ancestor, NULL, &aexceptpubids);
+
+ if (!list_member_oid(aexceptpubids, pub->oid))
+ {
+ pub_relid = ancestor;
+ ancestor_level = level;
+ }
+ }

Consider the following table structure:
t1 has a partition p1, which in turn has a child partition
child_part1. When publish_via_partition_root is set to true, any
changes made to child_part1 are replicated through t1. If we add t1 to
the EXCEPT list, get_rel_sync_entry() still marks p1 as an ancestor to
publish changes or child_part1. Is it correct?

6)
RelationBuildPublicationDesc() also needs some more analysis about
getting and setting ancestor part for above case.

7)
Currently the way we deal with the except table in pg_dump.c differs
from how we deal with included-table. To explain the same, how about
adding below comment in getPublications() just before we fetch
except-list:

We process EXCEPT TABLES here instead of in getPublicationTables(),
and output them directly in dumpPublication(). This differs from the
approach used in dumpPublicationTable() and
dumpPublicationNamespace(). Following that approach would require
dumping table additions later as ALTER PUBLICATION … ADD EXCEPT, which
is currently not supported.

thanks
Shveta

#172Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: shveta malik (#171)
1 attachment(s)
Re: Skipping schema changes in publication

On Fri, 26 Dec 2025 at 15:27, shveta malik <shveta.malik@gmail.com> wrote:

On Tue, Dec 23, 2025 at 12:03 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I have addressed the remaining comments, did some cosmetic changes and
addressed the comment shared by Shveta in [2].
[1]: /messages/by-id/CAA4eK1+rnjBOvkiQC2r4LuTwuje653iVPPAXcmJZXPpKvsNbOQ@mail.gmail.com
[2]: /messages/by-id/CAJpy0uCf5tXvqyVS3GQzU9J5HdSLAxX6Lxt1UKY4HJ8qnimCAw@mail.gmail.com

Thank You for the patch. Please find a few comments:

1)
GetTopMostAncestorInPublication():

+ if (list_member_oid(aexceptpubids, puboid))
+ {
+ list_free(aexceptpubids);
+ continue;
+ }

We need to do 'list_free(apubids)' as well here.

2)
GetTopMostAncestorInPublication(). Currently it has:

if (list_member_oid(aexceptpubids, puboid))
...
if (list_member_oid(apubids, puboid))
...
else
...schema mapping check

IMO more natural order of checks will be

if (list_member_oid(apubids, puboid))
..
else if (list_member_oid(aexceptpubids, puboid))
...
else
...schema mapping check

While analyzing this behavior, I noticed that changes to tables listed
in the EXCEPT clause were still being published. However, those
changes were not replicated, because pg_get_publication_tables()
correctly excluded those tables, and consequently pg_subscription_rel
was populated correctly on the subscriber side.
Even though replication was prevented downstream, we should not
publish changes for tables in the EXCEPT list in the first place.
Publishing them is unnecessary and inconsistent with the publication
definition. This issue has been addressed in the latest version of the
patch
And the above two changes are not required.

3)
+/*
+ * Return the list of relation OIDs excluded from a publication.
+ * This is only applicable for FOR ALL TABLES publications.
+ */
+List *
+GetPublicationExcludedRelations(Oid pubid, PublicationPartOpt pub_partopt)

a) Since now 'Relations' term means both tables and sequences, but
here we mean only Tables, we can rename it to have 'Tables' rather
than 'Relations'

b) Similar to GetAllPublicationRelations which is for 'ALL Tables'
pub, we can rename it to have 'All'

So the name can be 'GetAllPublicationExcludedTables' to be more clear.

Also we can move this function close to GetAllPublicationRelations as
it is more related to that.

4)
ObjectsInPublicationToOids()
+ case PUBLICATIONOBJ_EXCEPT_TABLE:
+ pubobj->pubtable->except = true;
+ *rels = lappend(*rels, pubobj->pubtable);
+ break;

Let me know when this will be hit when we already have
'ObjectsInAllPublicationToOids' in place?

I analysed it and found that we can eliminate the use of
'ObjectsInAllPublicationToOids'. This requires a change in gram.y.
Made the changes in the latest patch.

5)
get_rel_sync_entry():
+ level++;
+ GetRelationPublications(ancestor, NULL, &aexceptpubids);
+
+ if (!list_member_oid(aexceptpubids, pub->oid))
+ {
+ pub_relid = ancestor;
+ ancestor_level = level;
+ }
+ }

Consider the following table structure:
t1 has a partition p1, which in turn has a child partition
child_part1. When publish_via_partition_root is set to true, any
changes made to child_part1 are replicated through t1. If we add t1 to
the EXCEPT list, get_rel_sync_entry() still marks p1 as an ancestor to
publish changes or child_part1. Is it correct?

This change is not required. See reply to comment 1 and 2.

6)
RelationBuildPublicationDesc() also needs some more analysis about
getting and setting ancestor part for above case.

The case for partition tables can be tricky. Currently I am analysing
the behaviour of partitioned tables with the EXCEPT TABLE option in
general.
Will address it in the next version upon further analysing.

7)
Currently the way we deal with the except table in pg_dump.c differs
from how we deal with included-table. To explain the same, how about
adding below comment in getPublications() just before we fetch
except-list:

We process EXCEPT TABLES here instead of in getPublicationTables(),
and output them directly in dumpPublication(). This differs from the
approach used in dumpPublicationTable() and
dumpPublicationNamespace(). Following that approach would require
dumping table additions later as ALTER PUBLICATION … ADD EXCEPT, which
is currently not supported.

Also addressed the remaining comments. I have also addressed the
comments by Peter in [1]/messages/by-id/CAHut+PuZX_7Ot-oh5iqGLBRrZBS5ewDnHa91mJi2Y09uCRfixg@mail.gmail.com. I have also done some minor cosmetic
changes.
[1]: /messages/by-id/CAHut+PuZX_7Ot-oh5iqGLBRrZBS5ewDnHa91mJi2Y09uCRfixg@mail.gmail.com

Thanks,
Shlok Kyal

Attachments:

v35-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchapplication/octet-stream; name=v35-0001-Skip-publishing-the-tables-specified-in-EXCEPT-T.patchDownload
From 3a5a1d485c90a9697f192940a8fb119d8567511a Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Dec 2025 22:41:23 +0530
Subject: [PATCH v35] Skip publishing the tables specified in EXCEPT TABLE.

A new "EXCEPT TABLE" clause for CREATE PUBLICATION allows one or more
tables to be excluded. The publisher will not send the data of excluded
tables to the subscriber.

The new syntax allows specifying excluded relations when creating a
publication. For example:
CREATE PUBLICATION pub1 FOR ALL TABLES EXCEPT TABLE (t1,t2);

A new column "prexcept" is added to table "pg_publication_rel", to flag
the relations that the user wants to exclude from the publications.

pg_dump is updated to identify and dump the excluded tables of the publications.

The psql \d family of commands can now display excluded tables.

Bump catalog version.
---
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/logical-replication.sgml         |   6 +-
 doc/src/sgml/ref/create_publication.sgml      |  56 ++++-
 doc/src/sgml/ref/psql-ref.sgml                |  11 +-
 src/backend/catalog/pg_publication.c          | 143 +++++++++--
 src/backend/commands/publicationcmds.c        | 109 ++++----
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/parser/gram.y                     |  42 +++-
 src/backend/replication/pgoutput/pgoutput.c   |  43 +++-
 src/backend/utils/cache/relcache.c            |  21 +-
 src/bin/pg_dump/pg_dump.c                     |  71 ++++++
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  30 +++
 src/bin/psql/describe.c                       |  87 ++++++-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_publication.h          |  13 +-
 src/include/catalog/pg_publication_rel.h      |   1 +
 src/include/nodes/parsenodes.h                |   3 +
 src/test/regress/expected/publication.out     |  93 ++++++-
 src/test/regress/sql/publication.sql          |  37 ++-
 src/test/subscription/meson.build             |   1 +
 .../t/037_rep_changes_except_table.pl         | 238 ++++++++++++++++++
 22 files changed, 899 insertions(+), 133 deletions(-)
 create mode 100644 src/test/subscription/t/037_rep_changes_except_table.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2fc63442980..9e847152b44 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -6581,6 +6581,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       if there is no publication qualifying condition.</para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+      <structfield>prexcept</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if the relation is excluded from the publication. See
+       <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>prattrs</structfield> <type>int2vector</type>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 58ce75d8b63..17dd2f722fc 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -116,7 +116,11 @@
    <literal>FOR TABLES IN SCHEMA</literal>, <literal>FOR ALL TABLES</literal>,
    or <literal>FOR ALL SEQUENCES</literal>. Unlike tables, sequences can be
    synchronized at any time. For more information, see
-   <xref linkend="logical-replication-sequences"/>.
+   <xref linkend="logical-replication-sequences"/>. When a publication is
+   created with <literal>FOR ALL TABLES</literal>, tables can be explicitly
+   excluded from publication using the
+   <link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>
+   clause.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 75a508bebfa..e1f5c6a3938 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
 <phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
 
-    ALL TABLES
+    ALL TABLES [ EXCEPT [ TABLE ] ( <replaceable class="parameter">except_table_object</replaceable> [, ... ] ) ]
     ALL SEQUENCES
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
+
+<phrase>and <replaceable class="parameter">except_table_object</replaceable> is:</phrase>
+
+    [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -164,7 +168,8 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     <listitem>
      <para>
       Marks the publication as one that replicates changes for all tables in
-      the database, including tables created in the future.
+      the database, including tables created in the future. Tables listed in
+      EXCEPT TABLE are excluded from the publication.
      </para>
     </listitem>
    </varlistentry>
@@ -184,6 +189,36 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createpublication-params-for-except-table">
+    <term><literal>EXCEPT TABLE</literal></term>
+    <listitem>
+     <para>
+      This clause specifies a list of tables to be excluded from the
+      publication.
+     </para>
+     <para>
+      For inherited tables, if <literal>ONLY</literal> is specified before the
+      table name, only that table is excluded from the publication. If
+      <literal>ONLY</literal> is not specified, the table and all its descendant
+      tables (if any) are excluded. Optionally, <literal>*</literal> can be
+      specified after the table name to explicitly indicate that descendant
+      tables are excluded.
+     </para>
+     <para>
+      For partitioned tables, when <literal>publish_via_partition_root</literal>
+      is set to <literal>true</literal>, specifying a root partitioned table in
+      <literal>EXCEPT TABLE</literal> excludes it and all its partitions from
+      replication. Specifying a leaf partition has no effect, as its changes are
+      still replicated via the root partitioned table. When
+      <literal>publish_via_partition_root</literal> is set to
+      <literal>false</literal>, specifying a root partitioned table has no
+      effect, as changes are replicated via the leaf partitions. Specifying a
+      leaf partition excludes only that partition from replication. The optional
+      <literal>*</literal> has no meaning for partitioned tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createpublication-params-with">
     <term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
     <listitem>
@@ -487,6 +522,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES;
    all sequences for synchronization:
 <programlisting>
 CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES;
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all changes in all tables except
+   <structname>users</structname> and <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT (users, departments);
+</programlisting>
+  </para>
+
+  <para>
+   Create a publication that publishes all sequences for synchronization, and
+   all changes in all tables except <structname>users</structname> and
+   <structname>departments</structname>:
+<programlisting>
+CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT (users, departments);
 </programlisting>
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f56c70263e0..8416128a9ad 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1299,10 +1299,8 @@ SELECT $1 \parse stmt1
         <replaceable class="parameter">pattern</replaceable>, show all
         columns, their types, the tablespace (if not the default) and any
         special attributes such as <literal>NOT NULL</literal> or defaults.
-        Associated indexes, constraints, rules, and triggers are
-        also shown.  For foreign tables, the associated foreign
-        server is shown as well.
-        (<quote>Matching the pattern</quote> is defined in
+        Associated indexes, constraints, rules, publications, and triggers are
+        also shown. (<quote>Matching the pattern</quote> is defined in
         <xref linkend="app-psql-patterns"/> below.)
         </para>
 
@@ -2103,8 +2101,9 @@ SELECT $1 \parse stmt1
         listed.
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
-        If <literal>+</literal> is appended to the command name, the tables and
-        schemas associated with each publication are shown as well.
+        If <literal>+</literal> is appended to the command name, the tables,
+        excluded tables, and schemas associated with each publication are shown
+        as well.
         </para>
         </listitem>
       </varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9a4791c573e..09c69005122 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -366,9 +366,11 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	foreach(lc, ancestors)
 	{
 		Oid			ancestor = lfirst_oid(lc);
-		List	   *apubids = GetRelationPublications(ancestor);
+		List	   *apubids = NIL;
 		List	   *aschemaPubids = NIL;
 
+		GetRelationPublications(ancestor, &apubids, NULL);
+
 		level++;
 
 		if (list_member_oid(apubids, puboid))
@@ -466,6 +468,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 						RelationGetRelationName(targetrel), pub->name)));
 	}
 
+	/*
+	 * Handle the case where a partition is excluded by EXCEPT TABLE while
+	 * publish_via_partition_root = true.
+	 */
+	if (pub->alltables && pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relispartition)
+		ereport(WARNING,
+				(errmsg("partition \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "true")));
+
+	/*
+	 * Handle the case where a partitioned table is excluded by EXCEPT TABLE
+	 * while publish_via_partition_root = false.
+	 */
+	if (pub->alltables && !pub->pubviaroot && pri->except &&
+		targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		ereport(WARNING,
+				(errmsg("partitioned table \"%s\" might be replicated as publish_via_partition_root is \"%s\"",
+						RelationGetRelationName(targetrel), "false")));
+
 	check_publication_add_relation(targetrel);
 
 	/* Validate and translate column names into a Bitmapset of attnums. */
@@ -482,6 +504,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
 		ObjectIdGetDatum(pubid);
 	values[Anum_pg_publication_rel_prrelid - 1] =
 		ObjectIdGetDatum(relid);
+	values[Anum_pg_publication_rel_prexcept - 1] =
+		BoolGetDatum(pri->except);
 
 	/* Add qualifications, if available */
 	if (pri->whereClause != NULL)
@@ -749,38 +773,58 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
 	return myself;
 }
 
-/* Gets list of publication oids for a relation */
-List *
-GetRelationPublications(Oid relid)
+/*
+ * Get the list of publication oids associated with a specified relation.
+ *
+ * Parameter 'pubids' returns the Oids of the publications the relation is part
+ * of. Parameter 'except_pubids' returns the Oids of publications the relation
+ * is excluded from.
+ *
+ * This function returns true if the relation is part of any publication.
+ */
+bool
+GetRelationPublications(Oid relid, List **pubids, List **except_pubids)
 {
-	List	   *result = NIL;
 	CatCList   *pubrellist;
-	int			i;
+	bool		found = false;
 
 	/* Find all publications associated with the relation. */
 	pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
 									 ObjectIdGetDatum(relid));
-	for (i = 0; i < pubrellist->n_members; i++)
+	for (int i = 0; i < pubrellist->n_members; i++)
 	{
 		HeapTuple	tup = &pubrellist->members[i]->tuple;
-		Oid			pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
+		Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
+		Oid			pubid = pubrel->prpubid;
 
-		result = lappend_oid(result, pubid);
+		if (pubrel->prexcept)
+		{
+			if (except_pubids)
+				*except_pubids = lappend_oid(*except_pubids, pubid);
+		}
+		else
+		{
+			if (pubids)
+				*pubids = lappend_oid(*pubids, pubid);
+			found = true;
+		}
 	}
 
 	ReleaseSysCacheList(pubrellist);
 
-	return result;
+	return found;
 }
 
 /*
- * Gets list of relation oids for a publication.
+ * Internal function to get the list of relation Oids for a publication.
  *
- * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
- * should use GetAllPublicationRelations().
+ * If except_flag is true, returns the list of relations excluded from the
+ * publication; otherwise, returns the list of relations included in the
+ * publication.
  */
-List *
-GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
+static List *
+get_publication_relations(Oid pubid, PublicationPartOpt pub_partopt,
+						  bool except_flag)
 {
 	List	   *result;
 	Relation	pubrelsrel;
@@ -805,8 +849,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 		Form_pg_publication_rel pubrel;
 
 		pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
-		result = GetPubPartitionOptionRelations(result, pub_partopt,
-												pubrel->prrelid);
+
+		if (except_flag == pubrel->prexcept)
+			result = GetPubPartitionOptionRelations(result, pub_partopt,
+													pubrel->prrelid);
 	}
 
 	systable_endscan(scan);
@@ -819,6 +865,36 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
 	return result;
 }
 
+/*
+ * Return the list of relation Oids for a publication.
+ *
+ * For a FOR TABLE publication, this returns the list of relations explicitly
+ * included in the publication.
+ *
+ * Publications declared with FOR ALL TABLES should use
+ * GetAllPublicationRelations() to obtain the complete set of tables covered by
+ * the publication.
+ */
+List *
+GetPublicationIncludedRelations(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(!GetPublication(pubid)->alltables);
+
+	return get_publication_relations(pubid, pub_partopt, false);
+}
+
+/*
+ * Return the list of tables Oids excluded from a publication.
+ * This is only applicable for FOR ALL TABLES publications.
+ */
+List *
+GetAllPublicationExcludedTables(Oid pubid, PublicationPartOpt pub_partopt)
+{
+	Assert(GetPublication(pubid)->alltables);
+
+	return get_publication_relations(pubid, pub_partopt, true);
+}
+
 /*
  * Gets list of publication oids for publications marked as FOR ALL TABLES.
  */
@@ -864,18 +940,29 @@ GetAllTablesPublications(void)
  * partitioned tables, we must exclude partitions in favor of including the
  * root partitioned tables. This is not applicable to FOR ALL SEQUENCES
  * publication.
+ *
+ * For a FOR ALL TABLES publication, the returned list excludes tables mentioned
+ * in EXCEPT TABLE clause.
  */
 List *
-GetAllPublicationRelations(char relkind, bool pubviaroot)
+GetAllPublicationRelations(Publication *pub, char relkind)
 {
 	Relation	classRel;
 	ScanKeyData key[1];
 	TableScanDesc scan;
 	HeapTuple	tuple;
 	List	   *result = NIL;
+	List	   *exceptlist = NIL;
+	bool		pubviaroot = pub->pubviaroot;
+	Oid			pubid = pub->oid;
 
 	Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
 
+	if (relkind == RELKIND_RELATION)
+		exceptlist = GetAllPublicationExcludedTables(pubid, pubviaroot ?
+													 PUBLICATION_PART_ALL :
+													 PUBLICATION_PART_ROOT);
+
 	classRel = table_open(RelationRelationId, AccessShareLock);
 
 	ScanKeyInit(&key[0],
@@ -891,7 +978,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Oid			relid = relForm->oid;
 
 		if (is_publishable_class(relid, relForm) &&
-			!(relForm->relispartition && pubviaroot))
+			!(relForm->relispartition && pubviaroot) &&
+			!list_member_oid(exceptlist, relid))
 			result = lappend_oid(result, relid);
 	}
 
@@ -912,7 +1000,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Oid			relid = relForm->oid;
 
 			if (is_publishable_class(relid, relForm) &&
-				!relForm->relispartition)
+				!relForm->relispartition &&
+				!list_member_oid(exceptlist, relid))
 				result = lappend_oid(result, relid);
 		}
 
@@ -1168,17 +1257,17 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			 * those. Otherwise, get the partitioned table itself.
 			 */
 			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+				pub_elem_tables = GetAllPublicationRelations(pub_elem,
+															 RELKIND_RELATION);
 			else
 			{
 				List	   *relids,
 						   *schemarelids;
 
-				relids = GetPublicationRelations(pub_elem->oid,
-												 pub_elem->pubviaroot ?
-												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
+				relids = GetPublicationIncludedRelations(pub_elem->oid,
+														 pub_elem->pubviaroot ?
+														 PUBLICATION_PART_ROOT :
+														 PUBLICATION_PART_LEAF);
 				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
 																pub_elem->pubviaroot ?
 																PUBLICATION_PART_ROOT :
@@ -1367,7 +1456,7 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
 		publication = GetPublicationByName(pubname, false);
 
 		if (publication->allsequences)
-			sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
+			sequences = GetAllPublicationRelations(publication, RELKIND_SEQUENCE);
 
 		funcctx->user_fctx = sequences;
 
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index fc3a4c19e65..6bb816b219c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -198,7 +198,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
 
 		switch (pubobj->pubobjtype)
 		{
+			case PUBLICATIONOBJ_EXCEPT_TABLE:
+				pubobj->pubtable->except = true;
+				*rels = lappend(*rels, pubobj->pubtable);
+				break;
 			case PUBLICATIONOBJ_TABLE:
+				pubobj->pubtable->except = false;
 				*rels = lappend(*rels, pubobj->pubtable);
 				break;
 			case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
@@ -519,8 +524,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
 		 * a target. However, WAL records for TRUNCATE specify both a root and
 		 * its leaves.
 		 */
-		relids = GetPublicationRelations(pubid,
-										 PUBLICATION_PART_ALL);
+		relids = GetPublicationIncludedRelations(pubid,
+												 PUBLICATION_PART_ALL);
 		schemarelids = GetAllSchemaPublicationRelations(pubid,
 														PUBLICATION_PART_ALL);
 
@@ -929,55 +934,61 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 	CommandCounterIncrement();
 
 	/* Associate objects with the publication. */
-	if (stmt->for_all_tables)
+	ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+							   &schemaidlist);
+
+	/* FOR TABLES IN SCHEMA requires superuser */
+	if (schemaidlist != NIL && !superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
+
+	/* Add relations (tables) to the publication. */
+	if (relations != NIL)
 	{
 		/*
-		 * Invalidate relcache so that publication info is rebuilt. Sequences
-		 * publication doesn't require invalidation, as replica identity
-		 * checks don't apply to them.
+		 * The 'relations' list can be non-empty in only two cases:
+		 *
+		 * 1. CREATE PUBLICATION ... FOR TABLE In this case, 'relations'
+		 * contains the list of specified tables.
+		 *
+		 * 2. CREATE PUBLICATION ... FOR ALL TABLES In this case, 'relations'
+		 * contains the list of tables specified in the EXCEPT TABLE clause.
+		 * During parsing, the 'except' flag is set for the associated
+		 * PublicationRelInfo objects.
 		 */
-		CacheInvalidateRelcacheAll();
-	}
-	else if (!stmt->for_all_sequences)
-	{
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &schemaidlist);
+		List	   *rels;
 
-		/* FOR TABLES IN SCHEMA requires superuser */
-		if (schemaidlist != NIL && !superuser())
-			ereport(ERROR,
-					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-					errmsg("must be superuser to create FOR TABLES IN SCHEMA publication"));
-
-		if (relations != NIL)
-		{
-			List	   *rels;
+		rels = OpenTableList(relations);
+		TransformPubWhereClauses(rels, pstate->p_sourcetext,
+								 publish_via_partition_root);
 
-			rels = OpenTableList(relations);
-			TransformPubWhereClauses(rels, pstate->p_sourcetext,
-									 publish_via_partition_root);
+		CheckPubRelationColumnList(stmt->pubname, rels,
+								   schemaidlist != NIL,
+								   publish_via_partition_root);
 
-			CheckPubRelationColumnList(stmt->pubname, rels,
-									   schemaidlist != NIL,
-									   publish_via_partition_root);
-
-			PublicationAddTables(puboid, rels, true, NULL);
-			CloseTableList(rels);
-		}
+		PublicationAddTables(puboid, rels, true, NULL);
+		CloseTableList(rels);
+	}
 
-		if (schemaidlist != NIL)
-		{
-			/*
-			 * Schema lock is held until the publication is created to prevent
-			 * concurrent schema deletion.
-			 */
-			LockSchemaList(schemaidlist);
-			PublicationAddSchemas(puboid, schemaidlist, true, NULL);
-		}
+	if (schemaidlist != NIL)
+	{
+		/*
+		 * Schema lock is held until the publication is created to prevent
+		 * concurrent schema deletion.
+		 */
+		LockSchemaList(schemaidlist);
+		PublicationAddSchemas(puboid, schemaidlist, true, NULL);
 	}
 
 	table_close(rel, RowExclusiveLock);
 
+	if (stmt->for_all_tables)
+	{
+		/* Invalidate relcache so that publication info is rebuilt. */
+		CacheInvalidateRelcacheAll();
+	}
+
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
 	/*
@@ -1050,8 +1061,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
 						   AccessShareLock);
 
-		root_relids = GetPublicationRelations(pubform->oid,
-											  PUBLICATION_PART_ROOT);
+		root_relids = GetPublicationIncludedRelations(pubform->oid,
+													  PUBLICATION_PART_ROOT);
 
 		foreach(lc, root_relids)
 		{
@@ -1170,8 +1181,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		 * trees, not just those explicitly mentioned in the publication.
 		 */
 		if (root_relids == NIL)
-			relids = GetPublicationRelations(pubform->oid,
-											 PUBLICATION_PART_ALL);
+			relids = GetPublicationIncludedRelations(pubform->oid,
+													 PUBLICATION_PART_ALL);
 		else
 		{
 			/*
@@ -1256,8 +1267,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		PublicationDropTables(pubid, rels, false);
 	else						/* AP_SetObjects */
 	{
-		List	   *oldrelids = GetPublicationRelations(pubid,
-														PUBLICATION_PART_ROOT);
+		List	   *oldrelids = GetPublicationIncludedRelations(pubid,
+																PUBLICATION_PART_ROOT);
 		List	   *delrels = NIL;
 		ListCell   *oldlc;
 
@@ -1358,6 +1369,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 				oldrel = palloc_object(PublicationRelInfo);
 				oldrel->whereClause = NULL;
 				oldrel->columns = NIL;
+				oldrel->except = false;
 				oldrel->relation = table_open(oldrelid,
 											  ShareUpdateExclusiveLock);
 				delrels = lappend(delrels, oldrel);
@@ -1408,7 +1420,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		ListCell   *lc;
 		List	   *reloids;
 
-		reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+		reloids = GetPublicationIncludedRelations(pubform->oid,
+												  PUBLICATION_PART_ROOT);
 
 		foreach(lc, reloids)
 		{
@@ -1771,6 +1784,7 @@ OpenTableList(List *tables)
 		pub_rel->relation = rel;
 		pub_rel->whereClause = t->whereClause;
 		pub_rel->columns = t->columns;
+		pub_rel->except = t->except;
 		rels = lappend(rels, pub_rel);
 		relids = lappend_oid(relids, myrelid);
 
@@ -1843,6 +1857,7 @@ OpenTableList(List *tables)
 
 				/* child inherits column list from parent */
 				pub_rel->columns = t->columns;
+				pub_rel->except = t->except;
 				rels = lappend(rels, pub_rel);
 				relids = lappend_oid(relids, childrelid);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f976c0e5c7e..a5351fc59c6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8688,7 +8688,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * expressions.
 	 */
 	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
@@ -18884,7 +18884,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	 * UNLOGGED, as UNLOGGED tables can't be published.
 	 */
 	if (!toLogged &&
-		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		GetRelationPublications(RelationGetRelid(rel), NULL, NULL))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 713ee5c10a2..d3c3e9f59c6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -203,6 +203,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
 												core_yyscan_t yyscanner);
 static void preprocess_pub_all_objtype_list(List *all_objects_list,
+											List **pubobjects,
 											bool *all_tables,
 											bool *all_sequences,
 											core_yyscan_t yyscanner);
@@ -455,6 +456,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list pub_all_obj_type_list
+				pub_except_obj_list opt_pub_except_clause
 
 %type <retclause> returning_clause
 %type <node>	returning_option
@@ -592,6 +594,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 <publicationobjectspec> PublicationExceptObjSpec
 %type <publicationallobjectspec> PublicationAllObjSpec
 
 %type <keyword> unreserved_keyword type_func_name_keyword
@@ -10792,7 +10795,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
  *
  * pub_all_obj_type is one of:
  *
- *		TABLES
+ *		TABLES [EXCEPT [TABLE] ( table [, ...] )]
  *		SEQUENCES
  *
  * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
@@ -10818,7 +10821,8 @@ CreatePublicationStmt:
 					CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
 
 					n->pubname = $3;
-					preprocess_pub_all_objtype_list($5, &n->for_all_tables,
+					preprocess_pub_all_objtype_list($5, &n->pubobjects,
+													&n->for_all_tables,
 													&n->for_all_sequences,
 													yyscanner);
 					n->options = $6;
@@ -10858,6 +10862,7 @@ PublicationObjSpec:
 					$$->pubtable->relation = $2;
 					$$->pubtable->columns = $3;
 					$$->pubtable->whereClause = $4;
+					$$->location = @1;
 				}
 			| TABLES IN_P SCHEMA ColId
 				{
@@ -10933,11 +10938,19 @@ pub_obj_list:	PublicationObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+opt_pub_except_clause:
+			EXCEPT opt_table '(' pub_except_obj_list ')'	{ $$ = $4; }
+			| /*EMPTY*/										{ $$ = NIL; }
+		;
+
 PublicationAllObjSpec:
-				ALL TABLES
+				ALL TABLES opt_pub_except_clause
 					{
 						$$ = makeNode(PublicationAllObjSpec);
 						$$->pubobjtype = PUBLICATION_ALL_TABLES;
+						$$->except_tables = $3;
+						if($$->except_tables != NULL)
+							preprocess_pubobj_list($$->except_tables, yyscanner);
 						$$->location = @1;
 					}
 				| ALL SEQUENCES
@@ -10954,6 +10967,23 @@ pub_all_obj_type_list:	PublicationAllObjSpec
 					{ $$ = lappend($1, $3); }
 	;
 
+PublicationExceptObjSpec:
+			 relation_expr
+				{
+					$$ = makeNode(PublicationObjSpec);
+					$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
+					$$->pubtable = makeNode(PublicationTable);
+					$$->pubtable->except = true;
+					$$->pubtable->relation = $1;
+					$$->location = @1;
+				}
+	;
+
+pub_except_obj_list: PublicationExceptObjSpec
+					{ $$ = list_make1($1); }
+			| pub_except_obj_list ',' PublicationExceptObjSpec
+					{ $$ = lappend($1, $3); }
+	;
 
 /*****************************************************************************
  *
@@ -19794,8 +19824,9 @@ parsePartitionStrategy(char *strategy, int location, core_yyscan_t yyscanner)
  * Also, checks if the pub_object_type has been specified more than once.
  */
 static void
-preprocess_pub_all_objtype_list(List *all_objects_list, bool *all_tables,
-								bool *all_sequences, core_yyscan_t yyscanner)
+preprocess_pub_all_objtype_list(List *all_objects_list, List **pubobjects,
+								bool *all_tables, bool *all_sequences,
+								core_yyscan_t yyscanner)
 {
 	if (!all_objects_list)
 		return;
@@ -19815,6 +19846,7 @@ preprocess_pub_all_objtype_list(List *all_objects_list, bool *all_tables,
 						parser_errposition(obj->location));
 
 			*all_tables = true;
+			*pubobjects = list_concat(*pubobjects, obj->except_tables);
 		}
 		else if (obj->pubobjtype == PUBLICATION_ALL_SEQUENCES)
 		{
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 9ee8949e040..fdc1d12b32c 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2088,7 +2088,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	if (!entry->replicate_valid)
 	{
 		Oid			schemaId = get_rel_namespace(relid);
-		List	   *pubids = GetRelationPublications(relid);
+		List	   *pubids = NIL;
 
 		/*
 		 * We don't acquire a lock on the namespace system table as we build
@@ -2103,6 +2103,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		char		relkind = get_rel_relkind(relid);
 		List	   *rel_publications = NIL;
 
+		GetRelationPublications(relid, &pubids, NULL);
+
 		/* Reload publications if needed before use. */
 		if (!publications_valid)
 		{
@@ -2202,10 +2204,26 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			/*
 			 * If this is a FOR ALL TABLES publication, pick the partition
 			 * root and set the ancestor level accordingly.
+			 *
+			 * If this is a FOR ALL TABLES publication and it has an EXCEPT
+			 * TABLE list:
+			 *
+			 * 1. If pubviaroot is set and the relation is a partition, check
+			 * whether the partition root is included in the EXCEPT TABLE
+			 * list. If so, do not publish the change.
+			 *
+			 * 2. If pubviaroot is not set, check whether the relation itself
+			 * is included in the EXCEPT TABLE list. If so, do not publish the
+			 * change.
+			 *
+			 * This is achieved by keeping the variable "publish" set to
+			 * false. And eventually, entry->pubactions will remain all false
+			 * for this publication.
 			 */
 			if (pub->alltables)
 			{
-				publish = true;
+				List	   *exceptpubids = NIL;
+
 				if (pub->pubviaroot && am_partition)
 				{
 					List	   *ancestors = get_partition_ancestors(relid);
@@ -2213,9 +2231,23 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 					pub_relid = llast_oid(ancestors);
 					ancestor_level = list_length(ancestors);
 				}
-			}
 
-			if (!publish)
+				GetRelationPublications(pub_relid, NULL, &exceptpubids);
+
+				if (!list_member_oid(exceptpubids, pub->oid))
+					publish = true;
+				else
+				{
+					/* Sanity check */
+					Assert(entry->pubactions.pubinsert == false &&
+						   entry->pubactions.pubupdate == false &&
+						   entry->pubactions.pubdelete == false &&
+						   entry->pubactions.pubtruncate == false);
+				}
+
+				list_free(exceptpubids);
+			}
+			else if (!publish)
 			{
 				bool		ancestor_published = false;
 
@@ -2259,6 +2291,9 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			 * Don't publish changes for partitioned tables, because
 			 * publishing those of its partitions suffices, unless partition
 			 * changes won't be published due to pubviaroot being set.
+			 *
+			 * If the relation is part of EXCEPT TABLE list of a publication,
+			 * the 'publish' variable is already set to false.
 			 */
 			if (publish &&
 				(relkind != RELKIND_PARTITIONED_TABLE || pub->pubviaroot))
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 6b634c9fff1..dc021dbb6cd 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5788,7 +5788,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 void
 RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 {
-	List	   *puboids;
+	List	   *puboids = NIL;
+	List	   *exceptpuboids = NIL;
+	List	   *alltablespuboids;
 	ListCell   *lc;
 	MemoryContext oldcxt;
 	Oid			schemaid;
@@ -5826,7 +5828,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	pubdesc->gencols_valid_for_delete = true;
 
 	/* Fetch the publication membership info. */
-	puboids = GetRelationPublications(relid);
+	GetRelationPublications(relid, &puboids, &exceptpuboids);
 	schemaid = RelationGetNamespace(relation);
 	puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
 
@@ -5838,16 +5840,25 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 		foreach(lc, ancestors)
 		{
 			Oid			ancestor = lfirst_oid(lc);
+			List	   *ancestor_puboids = NIL;
+			List	   *ancestor_exceptpuboids = NIL;
 
-			puboids = list_concat_unique_oid(puboids,
-											 GetRelationPublications(ancestor));
+			GetRelationPublications(ancestor, &ancestor_puboids,
+									&ancestor_exceptpuboids);
+
+			puboids = list_concat_unique_oid(puboids, ancestor_puboids);
 			schemaid = get_rel_namespace(ancestor);
 			puboids = list_concat_unique_oid(puboids,
 											 GetSchemaPublications(schemaid));
+			exceptpuboids = list_concat_unique_oid(exceptpuboids,
+												   ancestor_exceptpuboids);
 		}
 	}
-	puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
 
+	alltablespuboids = GetAllTablesPublications();
+	puboids = list_concat_unique_oid(puboids,
+									 list_difference_oid(alltablespuboids,
+														 exceptpuboids));
 	foreach(lc, puboids)
 	{
 		Oid			pubid = lfirst_oid(lc);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7df56d8b1b0..f0d012c3629 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4634,9 +4634,58 @@ getPublications(Archive *fout)
 			(strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0);
 		pubinfo[i].pubgencols_type =
 			*(PQgetvalue(res, i, i_pubgencols));
+		pubinfo[i].except_tables = (SimplePtrList)
+		{
+			NULL, NULL
+		};
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(pubinfo[i].dobj), fout);
+
+		/*
+		 * Get the list of tables for publications specified with the EXCEPT
+		 * TABLE clause. This is introduced in PostgreSQL 19.
+		 *
+		 * EXCEPT TABLES is processed here and output directly by
+		 * dumpPublication(). This differs from the approach used in
+		 * dumpPublicationTable() and dumpPublicationNamespace(), since that
+		 * approach would require EXCEPT TABLE support for ALTER PUBLICATION,
+		 * which is not currently supported.
+		 */
+		if (fout->remoteVersion >= 190000)
+		{
+			int			ntbls;
+			PGresult   *res_tbls;
+
+			resetPQExpBuffer(query);
+			appendPQExpBuffer(query,
+							  "SELECT prrelid\n"
+							  "FROM pg_catalog.pg_publication_rel\n"
+							  "WHERE prpubid = %u and prexcept",
+							  pubinfo[i].dobj.catId.oid);
+
+			res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+			ntbls = PQntuples(res_tbls);
+			if (ntbls == 0)
+				continue;
+
+			for (int j = 0; j < ntbls; j++)
+			{
+				Oid			prrelid;
+				TableInfo  *tbinfo;
+
+				prrelid = atooid(PQgetvalue(res_tbls, j, 0));
+
+				tbinfo = findTableByOid(prrelid);
+				if (tbinfo == NULL)
+					continue;
+
+				simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo);
+			}
+
+			PQclear(res_tbls);
+		}
 	}
 
 cleanup:
@@ -4676,7 +4725,25 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	if (pubinfo->puballtables && pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
 	else if (pubinfo->puballtables)
+	{
+		int			n_excluded = 0;
+
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
+
+		/* Include EXCEPT TABLE clause if there are except_tables. */
+		for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell; cell = cell->next)
+		{
+			TableInfo  *tbinfo = (TableInfo *) cell->ptr;
+
+			if (++n_excluded == 1)
+				appendPQExpBufferStr(query, " EXCEPT TABLE (");
+			else
+				appendPQExpBufferStr(query, ", ");
+			appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
+		}
+		if (n_excluded > 0)
+			appendPQExpBufferStr(query, ")");
+	}
 	else if (pubinfo->puballsequences)
 		appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
 
@@ -4856,6 +4923,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	/* Collect all publication membership info. */
 	if (fout->remoteVersion >= 150000)
+	{
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
 							 "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
@@ -4868,6 +4936,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
 							 "      WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 							 "  ELSE NULL END) prattrs "
 							 "FROM pg_catalog.pg_publication_rel pr");
+		if (fout->remoteVersion >= 190000)
+			appendPQExpBufferStr(query, " WHERE NOT pr.prexcept");
+	}
 	else
 		appendPQExpBufferStr(query,
 							 "SELECT tableoid, oid, prpubid, prrelid, "
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4c4b14e5fc7..d141eb66d17 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -676,6 +676,7 @@ typedef struct _PublicationInfo
 	bool		pubtruncate;
 	bool		pubviaroot;
 	PublishGencolsType pubgencols_type;
+	SimplePtrList except_tables;
 } PublicationInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 28812d28aa9..4e6e4cbdd7c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3170,6 +3170,36 @@ my %tests = (
 		like => { %full_runs, section_post_data => 1, },
 	},
 
+	'CREATE PUBLICATION pub8' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (dump_test.test_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub9' => {
+		create_order => 50,
+		create_sql =>
+		  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
+	'CREATE PUBLICATION pub10' => {
+		create_order => 92,
+		create_sql =>
+		  'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_inheritance_parent);',
+		regexp => qr/^
+			\QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_inheritance_parent, ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E
+			/xm,
+		like => { %full_runs, section_post_data => 1, },
+	},
+
 	'CREATE SUBSCRIPTION sub1' => {
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub1
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 3584c4e1428..1bfec0bd3ef 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3073,17 +3073,34 @@ describeOneTableDetails(const char *schemaname,
 								  "          WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
 								  "        ELSE NULL END) "
 								  "FROM pg_catalog.pg_publication p\n"
-								  "     JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
-								  "     JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
-								  "WHERE pr.prrelid = '%s'\n"
+								  "		JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+								  "		JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prrelid = '%s'\n",
+								  oid, oid, oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n");
+
+				appendPQExpBuffer(&buf,
 								  "UNION\n"
 								  "SELECT pubname\n"
-								  "     , NULL\n"
-								  "     , NULL\n"
+								  "		, NULL\n"
+								  "		, NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
-								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
-								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n",
+								  oid);
+
+				if (pset.sversion >= 190000)
+					appendPQExpBuffer(&buf,
+									  "     AND NOT EXISTS (\n"
+									  "		SELECT 1\n"
+									  "		FROM pg_catalog.pg_publication_rel pr\n"
+									  "		JOIN pg_catalog.pg_class pc\n"
+									  "		ON pr.prrelid = pc.oid\n"
+									  "		WHERE pr.prrelid = '%s' AND pr.prpubid = p.oid)\n",
+									  oid);
+
+				appendPQExpBufferStr(&buf, "ORDER BY 1;");
 			}
 			else
 			{
@@ -3134,6 +3151,35 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* Print publications that the table is explicitly excluded from */
+		if (pset.sversion >= 190000)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT pubname\n"
+							  "FROM pg_catalog.pg_publication p\n"
+							  "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
+							  "WHERE pr.prrelid = '%s'\n AND pr.prexcept\n"
+							  "ORDER BY 1;", oid);
+
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+				printTableAddFooter(&cont, _("Except Publications:"));
+
+			/* Might be an empty set - that's ok */
+			for (i = 0; i < tuples; i++)
+			{
+				printfPQExpBuffer(&buf, "    \"%s\"", PQgetvalue(result, i, 0));
+
+				printTableAddFooter(&cont, buf.data);
+			}
+			PQclear(result);
+		}
+
 		/*
 		 * If verbose, print NOT NULL constraints.
 		 */
@@ -6753,8 +6799,12 @@ describePublications(const char *pattern)
 							  "     pg_catalog.pg_publication_rel pr\n"
 							  "WHERE c.relnamespace = n.oid\n"
 							  "  AND c.oid = pr.prrelid\n"
-							  "  AND pr.prpubid = '%s'\n"
-							  "ORDER BY 1,2", pubid);
+							  "  AND pr.prpubid = '%s'\n", pubid);
+
+			if (pset.sversion >= 190000)
+				appendPQExpBuffer(&buf, "  AND NOT pr.prexcept\n");
+
+			appendPQExpBuffer(&buf, "ORDER BY 1,2");
 			if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
 				goto error_return;
 
@@ -6772,6 +6822,23 @@ describePublications(const char *pattern)
 					goto error_return;
 			}
 		}
+		else
+		{
+			if (pset.sversion >= 190000)
+			{
+				/* Get the excluded tables for the specified publication */
+				printfPQExpBuffer(&buf,
+								  "SELECT concat(c.relnamespace::regnamespace, '.', c.relname)\n"
+								  "FROM pg_catalog.pg_class c\n"
+								  "     JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
+								  "WHERE pr.prpubid = '%s'\n"
+								  "  AND pr.prexcept\n"
+								  "ORDER BY 1", pubid);
+				if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
+												true, &cont))
+					goto error_return;
+			}
+		}
 
 		printTable(&cont, pset.queryFout, false, pset.logfile);
 		printTableCleanup(&cont);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 06edea98f06..8da2ec4869c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3660,7 +3660,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
 		COMPLETE_WITH("TABLES", "SEQUENCES");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
-		COMPLETE_WITH("WITH (");
+		COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
+		COMPLETE_WITH("TABLE (");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
 		COMPLETE_WITH("IN SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 368becca899..5126c996ff7 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -146,14 +146,16 @@ typedef struct PublicationRelInfo
 	Relation	relation;
 	Node	   *whereClause;
 	List	   *columns;
+	bool		except;
 } PublicationRelInfo;
 
 extern Publication *GetPublication(Oid pubid);
 extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
-extern List *GetRelationPublications(Oid relid);
+extern bool GetRelationPublications(Oid relid, List **pubids, List **except_pubids);
 
 /*---------
- * Expected values for pub_partopt parameter of GetPublicationRelations(),
+ * Expected values for pub_partopt parameter of
+ * GetPublicationIncludedRelations(), and GetAllPublicationExcludedTables(),
  * which allows callers to specify which partitions of partitioned tables
  * mentioned in the publication they expect to see.
  *
@@ -168,9 +170,12 @@ typedef enum PublicationPartOpt
 	PUBLICATION_PART_ALL,
 } PublicationPartOpt;
 
-extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
+extern List *GetPublicationIncludedRelations(Oid pubid,
+											 PublicationPartOpt pub_partopt);
+extern List *GetAllPublicationExcludedTables(Oid pubid,
+											 PublicationPartOpt pub_partopt);
 extern List *GetAllTablesPublications(void);
-extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
+extern List *GetAllPublicationRelations(Publication *pub, char relkind);
 extern List *GetPublicationSchemas(Oid pubid);
 extern List *GetSchemaPublications(Oid schemaid);
 extern List *GetSchemaPublicationRelations(Oid schemaid,
diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h
index 3a8790e8482..e3ccba5ec79 100644
--- a/src/include/catalog/pg_publication_rel.h
+++ b/src/include/catalog/pg_publication_rel.h
@@ -31,6 +31,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
 	Oid			oid;			/* oid */
 	Oid			prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
 	Oid			prrelid BKI_LOOKUP(pg_class);	/* Oid of the relation */
+	bool		prexcept BKI_DEFAULT(f);	/* exclude the relation */
 
 #ifdef	CATALOG_VARLEN			/* variable-length fields start here */
 	pg_node_tree prqual;		/* qualifications */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index aac4bfc70d9..761c74eb8bd 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4300,6 +4300,7 @@ typedef struct PublicationTable
 	RangeVar   *relation;		/* relation to be published */
 	Node	   *whereClause;	/* qualifications */
 	List	   *columns;		/* List of columns in a publication table */
+	bool		except;			/* exclude the relation */
 } PublicationTable;
 
 /*
@@ -4308,6 +4309,7 @@ typedef struct PublicationTable
 typedef enum PublicationObjSpecType
 {
 	PUBLICATIONOBJ_TABLE,		/* A table */
+	PUBLICATIONOBJ_EXCEPT_TABLE,	/* A table to be excluded */
 	PUBLICATIONOBJ_TABLES_IN_SCHEMA,	/* All tables in schema */
 	PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA,	/* All tables in first element of
 											 * search_path */
@@ -4336,6 +4338,7 @@ typedef struct PublicationAllObjSpec
 {
 	NodeTag		type;
 	PublicationAllObjType pubobjtype;	/* type of this publication object */
+	List	   *except_tables;	/* List of tables to be excluded */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PublicationAllObjSpec;
 
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 7fb49aaf29b..b2115edb3c3 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -213,33 +213,104 @@ Not-null constraints:
  regress_publication_user | t          | f             | t       | t       | f       | f         | none              | f
 (1 row)
 
-DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+\dRp+ testpub_foralltables_excepttable
+                                          Publication testpub_foralltables_excepttable
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+    "public.testpub_tbl2"
+
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+\dRp+ testpub_foralltables_excepttable1
+                                         Publication testpub_foralltables_excepttable1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl1"
+
+-- Check that the table description shows the publication list the table is
+-- excluded from
+\d testpub_tbl1
+                            Table "public.testpub_tbl1"
+ Column |  Type   | Collation | Nullable |                 Default                  
+--------+---------+-----------+----------+------------------------------------------
+ id     | integer |           | not null | nextval('testpub_tbl1_id_seq'::regclass)
+ data   | text    |           |          | 
+Indexes:
+    "testpub_tbl1_pkey" PRIMARY KEY, btree (id)
+Publications:
+    "testpub_foralltables"
+Except Publications:
+    "testpub_foralltables_excepttable"
+    "testpub_foralltables_excepttable1"
+
 RESET client_min_messages;
+DROP TABLE testpub_tbl2;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
 \dRp+ testpub3
                                                       Publication testpub3
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
-    "public.testpub_tbl3a"
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
 
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
 \dRp+ testpub4
                                                       Publication testpub4
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f
 Tables:
-    "public.testpub_tbl3"
+    "public.testpub_tbl_parent"
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+\dRp+ testpub5
+                                                      Publication testpub5
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+-- EXCEPT with '*': exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+\dRp+ testpub6
+                                                      Publication testpub6
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
+\dRp+ testpub7
+                                                      Publication testpub7
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f
+Except tables:
+    "public.testpub_tbl_parent"
+
+RESET client_min_messages;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 85b00bd67c8..5224da93d77 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -105,20 +105,41 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
 \d+ testpub_tbl2
 \dRp+ testpub_foralltables
 
+SET client_min_messages = 'ERROR';
+-- Exclude tables using FOR ALL TABLES EXCEPT TABLE (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
+\dRp+ testpub_foralltables_excepttable
+-- Exclude tables using FOR ALL TABLES EXCEPT (tablelist)
+CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (testpub_tbl1);
+\dRp+ testpub_foralltables_excepttable1
+-- Check that the table description shows the publication list the table is
+-- excluded from
+\d testpub_tbl1
+
+RESET client_min_messages;
 DROP TABLE testpub_tbl2;
-DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
+DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
 
-CREATE TABLE testpub_tbl3 (a int);
-CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
+CREATE TABLE testpub_tbl_parent (a int);
+CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
 SET client_min_messages = 'ERROR';
-CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
-CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
-RESET client_min_messages;
+CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
 \dRp+ testpub3
+CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
 \dRp+ testpub4
+-- Exclude parent table, omitting both of 'ONLY' and '*'
+CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
+\dRp+ testpub5
+-- EXCEPT with '*': exclude table and all descendants
+CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
+\dRp+ testpub6
+-- EXCEPT with ONLY: exclude table but not descendants
+CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
+\dRp+ testpub7
 
-DROP TABLE testpub_tbl3, testpub_tbl3a;
-DROP PUBLICATION testpub3, testpub4;
+RESET client_min_messages;
+DROP TABLE testpub_tbl_parent, testpub_tbl_child;
+DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
 
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index a4c7dbaff59..07282aa3c18 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_rep_changes_except_table.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_rep_changes_except_table.pl b/src/test/subscription/t/037_rep_changes_except_table.pl
new file mode 100644
index 00000000000..95904ddd005
--- /dev/null
+++ b/src/test/subscription/t/037_rep_changes_except_table.pl
@@ -0,0 +1,238 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Logical replication tests for EXCEPT TABLE publications
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+# ============================================
+# EXCEPT TABLE test cases for normal tables
+# ============================================
+# Create schemas and tables on publisher
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 AS SELECT generate_series(1,10) AS a;
+));
+
+# Create schemas and tables on subscriber
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SCHEMA sch1;
+	CREATE TABLE sch1.tab1 (a int);
+));
+
+# Setup logical replication
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_schema FOR ALL TABLES EXCEPT TABLE (sch1.tab1)"
+);
+# Create a logical replication slot
+$node_publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'pgoutput')");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_schema CONNECTION '$publisher_connstr' PUBLICATION tap_pub_schema"
+);
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher,
+	'tap_sub_schema');
+
+# Check the table data does not sync for excluded table
+my $result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||),
+	'check there is no initial data copied for the excluded table');
+
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.tab1 VALUES(generate_series(11,20))");
+
+# Verify that data inserted to excluded table is not published.
+$result = $node_publisher->safe_psql('postgres',
+	"SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tap_sub_schema')"
+);
+is($result, qq(t), 'check no changes for excluded table in replication slot');
+
+$node_publisher->wait_for_catchup('tap_sub_schema');
+
+# Verify that data inserted to the excluded table is not replicated.
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT count(*), min(a), max(a) FROM sch1.tab1");
+is($result, qq(0||), 'check replicated inserts on subscriber');
+
+# cleanup
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_schema");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_schema");
+
+# ============================================
+# EXCEPT TABLE test cases for partitioned tables
+# Check behavior of EXCEPT TABLE with publish_via_partition_root on a
+# partitioned table and its partitions.
+# ============================================
+# Setup partitioned table and partitions on the publisher that map to normal
+# tables on the subscriber
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int) PARTITION BY RANGE(a);
+	CREATE TABLE sch1.part1 PARTITION OF sch1.t1 FOR VALUES FROM (0) TO (5);
+	CREATE TABLE sch1.part2 PARTITION OF sch1.t1 FOR VALUES FROM (6) TO (10);
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE sch1.t1(a int);
+	CREATE TABLE sch1.part1(a int);
+	CREATE TABLE sch1.part2(a int);
+));
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = false
+# Excluding a partition while publish_via_partition_root = false prevents
+# replication of rows inserted into the partitioned table for that particular
+# partition.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on other partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = false
+# Excluding the partitioned table still allows rows inserted into the
+# partitioned table to be replicated via its partitions.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root = false);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is( $result, qq(1
+2), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is( $result, qq(6
+7), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+$node_publisher->safe_psql('postgres',
+	"SELECT slot_name FROM pg_replication_slot_advance('test_slot', pg_current_wal_lsn());"
+);
+
+# EXCEPT TABLE (sch1.t1) with publish_via_partition_root = true
+# When the partitioned table is excluded and publish_via_partition_root is true,
+# no rows from the table or its partitions are replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.t1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6);
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+# Verify that data inserted to the partitioned table is not published when it is
+# excluded with publish_via_partition_root = true.
+$result = $node_publisher->safe_psql('postgres',
+	"SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tap_pub_part')"
+);
+is($result, qq(t), 'check no changes for excluded table in replication slot');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1");
+is($result, qq(), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on first partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on second partition');
+
+$node_publisher->safe_psql('postgres', "TRUNCATE sch1.t1");
+$node_publisher->wait_for_catchup('tap_sub_part');
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part;");
+
+# EXCEPT TABLE (sch1.part1) with publish_via_partition_root = true
+# When a partition is excluded but publish_via_partition_root is true,
+# rows published through the partitioned table can still be replicated.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (sch1.part1) WITH (publish_via_partition_root);
+	INSERT INTO sch1.t1 VALUES (1), (6)
+));
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub_part');
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO sch1.t1 VALUES (2), (7);");
+$node_publisher->wait_for_catchup('tap_sub_part');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.t1 ORDER BY a");
+is( $result, qq(1
+2
+6
+7), 'check rows on partitioned table');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part1");
+is($result, qq(), 'check rows on excluded partition');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM sch1.part2");
+is($result, qq(), 'check rows on other partition');
+
+$node_subscriber->stop('fast');
+$node_publisher->stop('fast');
+
+done_testing();
-- 
2.34.1

#173Peter Smith
smithpb2250@gmail.com
In reply to: Shlok Kyal (#172)
Re: Skipping schema changes in publication

Hi Shlok.

Some review comments for the v35-0001 patch (code)

======
src/backend/replication/pgoutput/pgoutput.c

get_rel_sync_entry:

1.
  /*
  * If this is a FOR ALL TABLES publication, pick the partition
  * root and set the ancestor level accordingly.
+ *
+ * If this is a FOR ALL TABLES publication and it has an EXCEPT
+ * TABLE list:
+ *
+ * 1. If pubviaroot is set and the relation is a partition, check
+ * whether the partition root is included in the EXCEPT TABLE
+ * list. If so, do not publish the change.
+ *
+ * 2. If pubviaroot is not set, check whether the relation itself
+ * is included in the EXCEPT TABLE list. If so, do not publish the
+ * change.
+ *
+ * This is achieved by keeping the variable "publish" set to
+ * false. And eventually, entry->pubactions will remain all false
+ * for this publication.
  */

For that last para ("This is achieved by..."), it is unclear what
"This" is referring to. I think you mean like below:

SUGGESTION
Note - "do not publish the change" is achieved by...

======
src/bin/pg_dump/pg_dump.c

getPublications:

2.
+ ntbls = PQntuples(res_tbls);
+ if (ntbls == 0)
+ continue;
+
+ for (int j = 0; j < ntbls; j++)
+ {
+ Oid prrelid;
+ TableInfo  *tbinfo;
+
+ prrelid = atooid(PQgetvalue(res_tbls, j, 0));
+
+ tbinfo = findTableByOid(prrelid);
+ if (tbinfo == NULL)
+ continue;
+
+ simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo);
+ }
+
+ PQclear(res_tbls);

2a.
That first condition with 'continue' looks like it would be better
just to remove it. Otherwise, the PQclear(res_tbls) may leak. Anyway,
the loop will never iterate if ntbls is 0, so where is the harm in
removing this?

~

2b.
Also, that "if (tbinfo == NULL)" seems overkill because it is only
avoiding the final statement of the loop. It might be better to
replace this like below:

if (tblinfo != NULL)
simple_ptr_list_append(...);

~~~

dumpPublication:

3.
+ appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));

I think that unconditionally choosing "ONLY" here may not be the right
thing to do, particularly when the excluded table is a partitioned
table. (e.g. this is related to off-list discussions about how to
EXCEPT partition tables).

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