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

