v7-0003-Add-EXCEPT-support-to-ALTER-PUBLICATION-SET-TABLE.patch

application/x-patch

Filename: v7-0003-Add-EXCEPT-support-to-ALTER-PUBLICATION-SET-TABLE.patch
Type: application/x-patch
Part: 2
Message: Re: Support EXCEPT for TABLES IN SCHEMA publications
From 85d685fffef948dd6917273b8001cdc6da58e232 Mon Sep 17 00:00:00 2001
From: Nisha Moond <nisha.moond412@gmail.com>
Date: Thu, 28 May 2026 12:14:31 +0530
Subject: [PATCH v7 3/3] Add EXCEPT support to ALTER PUBLICATION SET TABLES IN
 SCHEMA

Extend AlterPublicationExceptTables() with the AP_SetObjects case,
which redefines the publication and replaces the entire EXCEPT list.

Syntax:
ALTER PUBLICATION pub SET TABLES IN SCHEMA s EXCEPT (TABLE t1);

This patch also cleans up EXCEPT entries when a schema is dropped
from the publication.
---
 doc/src/sgml/ref/alter_publication.sgml     |  27 +++-
 src/backend/commands/publicationcmds.c      | 131 ++++++++++++++++++--
 src/backend/replication/pgoutput/pgoutput.c |  10 +-
 src/bin/psql/tab-complete.in.c              |  15 +++
 src/test/regress/expected/publication.out   |  82 +++++++++++-
 src/test/regress/sql/publication.sql        |  34 +++++
 src/test/subscription/t/037_except.pl       |  85 +++++++++++++
 7 files changed, 355 insertions(+), 29 deletions(-)

diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml
index 73f6375a66f..80b038e4b2e 100644
--- a/doc/src/sgml/ref/alter_publication.sgml
+++ b/doc/src/sgml/ref/alter_publication.sgml
@@ -97,7 +97,11 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
    used with a publication defined with <literal>FOR TABLE</literal> or
    <literal>FOR TABLES IN SCHEMA</literal>, replaces the list of tables/schemas
    in the publication with the specified list; the existing tables or schemas
-   that were present in the publication will be removed.
+   that were present in the publication will be removed.  When
+   <literal>SET TABLES IN SCHEMA</literal> is used with an
+   <literal>EXCEPT</literal> clause, the excluded tables for each schema are
+   replaced with the specified list; if <literal>EXCEPT</literal> is omitted
+   for a schema, any existing exclusions for that schema are cleared.
   </para>
 
   <para>
@@ -116,10 +120,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
 
   <para>
    The <literal>EXCEPT</literal> clause can be used with
-   <literal>ADD TABLES IN SCHEMA</literal> to exclude specific tables from the
-   publication. Using <literal>DROP TABLES IN SCHEMA</literal> on a publication
-   will automatically also remove any associated <literal>EXCEPT</literal>
-   entries.
+   <literal>ADD TABLES IN SCHEMA</literal> to exclude specific tables from a
+   schema-level publication.
   </para>
 
   <para>
@@ -214,7 +216,8 @@ ALTER PUBLICATION <replaceable class="parameter">name</replaceable> RENAME TO <r
     <term><literal>EXCEPT ( <replaceable class="parameter">except_table_object</replaceable> [, ... ] )</literal></term>
     <listitem>
      <para>
-      When used with <literal>ADD TABLES IN SCHEMA</literal>, specifies
+      When used with <literal>ADD TABLES IN SCHEMA</literal>
+      or <literal>SET TABLES IN SCHEMA</literal>, specifies
       tables to be excluded from the publication.  Each named
       table must belong to the schema specified in the same
       <literal>TABLES IN SCHEMA</literal> clause.  Table names may be
@@ -326,6 +329,18 @@ ALTER PUBLICATION sales_publication ADD TABLES IN SCHEMA sales EXCEPT (TABLE int
 </programlisting>
   </para>
 
+  <para>
+   Replace the schema list of <structname>sales_publication</structname> with
+   <structname>sales</structname>, excluding only
+   <structname>sales.drafts</structname>. Other than
+   <structname>sales.drafts</structname>, any previously excluded tables for schema
+   <structname>sales</structname> are no longer excluded. Any schemas previously in
+   <structname>sales_publication</structname> are removed:
+<programlisting>
+ALTER PUBLICATION sales_publication SET TABLES IN SCHEMA sales EXCEPT (TABLE drafts);
+</programlisting>
+  </para>
+
   <para>
    Add tables <structname>users</structname>,
    <structname>departments</structname> and schema
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index f23893bbd10..58e0c938f83 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1582,9 +1582,11 @@ AlterPublicationSchemaExceptTables(AlterPublicationStmt *stmt,
 	Oid			pubid = pubform->oid;
 
 	/*
-	 * Nothing to do if no EXCEPT entries.
+	 * Nothing to do if there are no EXCEPT entries, unless handling the SET
+	 * command, because if the user has removed all exceptions we need to
+	 * drop any existing ones.
 	 */
-	if (!except_rel_names)
+	if (!except_rel_names && stmt->action != AP_SetObjects)
 		return;
 
 	/*
@@ -1597,7 +1599,7 @@ AlterPublicationSchemaExceptTables(AlterPublicationStmt *stmt,
 
 	/*
 	 * EXCEPT is not meaningful with DROP: dropping a schema from a
-	 * publication already removes all its except entries via cascade, and
+	 * publication already removes all its EXCEPT entries via cascade, and
 	 * there is no sensible interpretation of "drop only the except entry but
 	 * keep the schema".
 	 */
@@ -1606,16 +1608,6 @@ AlterPublicationSchemaExceptTables(AlterPublicationStmt *stmt,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("EXCEPT clause is not supported with DROP in ALTER PUBLICATION")));
 
-	/*
-	 * XXX EXCEPT with SET is not currently implemented.  Workaround: DROP and
-	 * re-ADD the schema with the desired EXCEPT list.
-	 */
-	if (stmt->action == AP_SetObjects)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("EXCEPT clause is not supported with SET in ALTER PUBLICATION"),
-				 errhint("Drop and re-add the schema with the desired EXCEPT list.")));
-
 	if (stmt->action == AP_AddObjects)
 	{
 		List	   *rels;
@@ -1643,6 +1635,86 @@ AlterPublicationSchemaExceptTables(AlterPublicationStmt *stmt,
 
 		PublicationAddTables(pubid, rels, false, stmt);
 
+		CloseTableList(rels);
+	}
+	else						/* AP_SetObjects */
+	{
+		List	   *oldexceptrelids = NIL;
+		List	   *newexceptrelids = NIL;
+		List	   *delrelids = NIL;
+		List	   *rels;
+		List	   *explicitrelids;
+
+		rels = OpenTableList(except_rel_names);
+
+		/* Collect OIDs of the desired new EXCEPT list. */
+		foreach_ptr(PublicationRelInfo, pri, rels)
+		{
+			newexceptrelids = lappend_oid(newexceptrelids,
+										  RelationGetRelid(pri->relation));
+		}
+
+		explicitrelids = GetIncludedPublicationRelations(pubid,
+														 PUBLICATION_PART_ROOT);
+
+		/*
+		 * Validate that each excluded table is not also in the explicit table
+		 * list (which would be contradictory).
+		 */
+		foreach_ptr(PublicationRelInfo, pri, rels)
+		{
+			Oid			relid = RelationGetRelid(pri->relation);
+
+			if (list_member_oid(explicitrelids, relid))
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("table \"%s\" cannot appear in both the table list and the EXCEPT clause",
+							   RelationGetQualifiedRelationName(pri->relation)));
+		}
+
+		/*
+		 * Get the current set of EXCEPT entries.  Only FOR ALL TABLES and
+		 * schema-level publications can have EXCEPT entries; for any other
+		 * publication type oldexceptrelids stays NIL.
+		 *
+		 * Note: we check is_schema_publication() against the current catalog
+		 * state (before AlterPublicationSchemas has run), so if the caller is
+		 * doing SET TABLE t1 to convert a schema publication into a plain
+		 * table publication, is_schema_publication() still returns true here.
+		 * That is intentional: it lets us discover and clean up any stale
+		 * EXCEPT entries that belong to the old schema definition.
+		 */
+		if (GetPublication(pubid)->alltables || is_schema_publication(pubid))
+			oldexceptrelids = GetExcludedPublicationTables(pubid,
+														   PUBLICATION_PART_ROOT);
+
+		/* Build a list of old EXCEPT entries not present in the new list. */
+		foreach_oid(oldrelid, oldexceptrelids)
+		{
+			if (!list_member_oid(newexceptrelids, oldrelid))
+				delrelids = lappend_oid(delrelids, oldrelid);
+		}
+
+		/* Drop old EXCEPT entries not present in the new list. */
+		foreach_oid(relid, delrelids)
+		{
+			Oid			proid;
+			ObjectAddress obj;
+
+			proid = GetSysCacheOid2(PUBLICATIONRELMAP,
+									Anum_pg_publication_rel_oid,
+									ObjectIdGetDatum(relid),
+									ObjectIdGetDatum(pubid));
+			if (!OidIsValid(proid))
+				continue;		/* already gone */
+
+			ObjectAddressSet(obj, PublicationRelRelationId, proid);
+			performDeletion(&obj, DROP_CASCADE, 0);
+		}
+
+		/* Add new EXCEPT entries, skipping any already present. */
+		PublicationAddTables(pubid, rels, true, stmt);
+
 		CloseTableList(rels);
 	}
 }
@@ -2292,6 +2364,7 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
 	foreach(lc, schemas)
 	{
 		Oid			schemaid = lfirst_oid(lc);
+		List	   *except_relids;
 
 		psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP,
 							   Anum_pg_publication_namespace_oid,
@@ -2308,8 +2381,40 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
 							get_namespace_name(schemaid))));
 		}
 
+		/*
+		 * Collect EXCEPT entries for tables belonging to this schema before
+		 * removing the schema entry.
+		 */
+		except_relids = GetExcludedPublicationTables(pubid, PUBLICATION_PART_ROOT);
+
 		ObjectAddressSet(obj, PublicationNamespaceRelationId, psid);
 		performDeletion(&obj, DROP_CASCADE, 0);
+
+		/*
+		 * Drop any prexcept rows for tables belonging to this schema. These
+		 * rows have no pg_depend entry pointing at the
+		 * pg_publication_namespace row, so they are not cascaded by the
+		 * performDeletion() call above and must be cleaned up explicitly.
+		 */
+		foreach_oid(relid, except_relids)
+		{
+			Oid			proid;
+
+			if (get_rel_namespace(relid) != schemaid)
+				continue;
+
+			proid = GetSysCacheOid2(PUBLICATIONRELMAP,
+									Anum_pg_publication_rel_oid,
+									ObjectIdGetDatum(relid),
+									ObjectIdGetDatum(pubid));
+			if (!OidIsValid(proid))
+				continue;		/* already gone */
+
+			ObjectAddressSet(obj, PublicationRelRelationId, proid);
+			performDeletion(&obj, DROP_CASCADE, 0);
+		}
+
+		list_free(except_relids);
 	}
 }
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index c5f17bf2338..785a045ead2 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2229,7 +2229,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			 */
 			if (pub->alltables)
 			{
-				List	   *exceptpubids = NIL;
+				List	   *except_pubids = NIL;
 
 				if (am_partition)
 				{
@@ -2252,7 +2252,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 					 * clause. Therefore, for a partition, exclusion must be
 					 * evaluated at the top-most ancestor.
 					 */
-					exceptpubids = GetRelationExcludedPublications(last_ancestor_relid);
+					except_pubids = GetRelationExcludedPublications(last_ancestor_relid);
 				}
 				else
 				{
@@ -2260,13 +2260,13 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 					 * For a regular table or a root partitioned table, check
 					 * exclusion on table itself.
 					 */
-					exceptpubids = GetRelationExcludedPublications(pub_relid);
+					except_pubids = GetRelationExcludedPublications(pub_relid);
 				}
 
-				if (!list_member_oid(exceptpubids, pub->oid))
+				if (!list_member_oid(except_pubids, pub->oid))
 					publish = true;
 
-				list_free(exceptpubids);
+				list_free(except_pubids);
 
 				if (!publish)
 					continue;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8db3e129928..11c87b1b006 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2379,6 +2379,21 @@ match_previous_words(int pattern_id,
 	}
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE", MatchAnyN) && !ends_with(prev_wd, ','))
 		COMPLETE_WITH(")");
+	/* After a single schema name in SET context, offer EXCEPT ( TABLE */
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "TABLES", "IN", "SCHEMA", MatchAny) &&
+			 !ends_with(prev_wd, ','))
+		COMPLETE_WITH("EXCEPT ( TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT"))
+		COMPLETE_WITH("( TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "("))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE"))
+	{
+		set_completion_reference(prev4_wd);
+		COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_tables_in_schema);
+	}
+	else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE", MatchAnyN) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH(")");
 	/* ALTER PUBLICATION <name> SET ( */
 	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "SET", "("))
 		COMPLETE_WITH("publish", "publish_generated_columns", "publish_via_partition_root");
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index c912cfcea00..ff1cb4bed1e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -587,8 +587,7 @@ ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test;
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
-Except tables:
-    "pub_test.testpub_tbl_s1"
+(1 row)
 
 -- ADD: unqualified name is implicitly qualified with the schema, not public
 ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s2);
@@ -600,25 +599,98 @@ ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TAB
 Tables from schemas:
     "pub_test"
 Except tables:
-    "pub_test.testpub_tbl_s1"
     "pub_test.testpub_tbl_s2"
 
 -- ADD: multiple excepted tables using unqualified names
 ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s1, testpub_tbl_s2);
-ERROR:  relation "pub_test.testpub_tbl_s1" cannot be added because it is excluded from publication "testpub_alter_except"
 \dRp+ testpub_alter_except
                                                        Publication testpub_alter_except
           Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
 --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
  regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "pub_test"
 Except tables:
     "pub_test.testpub_tbl_s1"
     "pub_test.testpub_tbl_s2"
 
+-- SET: replace the except list (keep same schema, different except table)
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2);
+\dRp+ testpub_alter_except
+                                                       Publication testpub_alter_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "pub_test"
+Except tables:
+    "pub_test.testpub_tbl_s2"
+
+-- fail: table in EXCEPT clause also appears in the explicit table list
+ALTER PUBLICATION testpub_alter_except SET TABLE pub_test.testpub_tbl_s1, TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1);
+ERROR:  table "pub_test.testpub_tbl_s1" cannot appear in both the table list and the EXCEPT clause
+-- error: except table's schema (public) not in the publication's schema list (pub_test)
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE public.testpub_tbl1);
+ERROR:  table "public.testpub_tbl1" in EXCEPT clause does not belong to schema "pub_test"
+LINE 1: ...xcept SET TABLES IN SCHEMA pub_test EXCEPT (TABLE public.tes...
+                                                             ^
+-- error: EXCEPT is not allowed with DROP
+ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2);
+ERROR:  EXCEPT clause is not supported with DROP in ALTER PUBLICATION
+-- SET: unqualified name in EXCEPT is implicitly qualified with the schema
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s1);
+\dRp+ testpub_alter_except
+                                                       Publication testpub_alter_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "pub_test"
+Except tables:
+    "pub_test.testpub_tbl_s1"
+
+-- SET without EXCEPT clears the existing except list
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test;
+\dRp+ testpub_alter_except
+                                                       Publication testpub_alter_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "pub_test"
+
+-- SET to a different schema removes old schema's EXCEPT entries
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s1);
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA public;
+\dRp+ testpub_alter_except
+                                                       Publication testpub_alter_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "public"
+
+-- fail: nonexistent table in EXCEPT clause (SET path)
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table);
+ERROR:  relation "pub_test.nonexistent_table" does not exist
+-- SET: multiple schemas each with their own EXCEPT clause
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1),
+                                                                      public EXCEPT (TABLE testpub_tbl1);
+\dRp+ testpub_alter_except
+                                                       Publication testpub_alter_except
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "pub_test"
+    "public"
+Except tables:
+    "pub_test.testpub_tbl_s1"
+    "public.testpub_tbl1"
+
 -- fail: non-existing table in EXCEPT clause
 ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test;
-ERROR:  tables from schema "pub_test" are not part of the publication
 ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table);
 ERROR:  relation "pub_test.nonexistent_table" does not exist
 -- fail: EXCEPT table belongs to a different schema
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 9f816effa70..d008114e05b 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -300,6 +300,40 @@ ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s1, testpub_tbl_s2);
 \dRp+ testpub_alter_except
 
+-- SET: replace the except list (keep same schema, different except table)
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2);
+\dRp+ testpub_alter_except
+
+-- fail: table in EXCEPT clause also appears in the explicit table list
+ALTER PUBLICATION testpub_alter_except SET TABLE pub_test.testpub_tbl_s1, TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1);
+
+-- error: except table's schema (public) not in the publication's schema list (pub_test)
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE public.testpub_tbl1);
+
+-- error: EXCEPT is not allowed with DROP
+ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s2);
+
+-- SET: unqualified name in EXCEPT is implicitly qualified with the schema
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s1);
+\dRp+ testpub_alter_except
+
+-- SET without EXCEPT clears the existing except list
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test;
+\dRp+ testpub_alter_except
+
+-- SET to a different schema removes old schema's EXCEPT entries
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE testpub_tbl_s1);
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA public;
+\dRp+ testpub_alter_except
+
+-- fail: nonexistent table in EXCEPT clause (SET path)
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table);
+
+-- SET: multiple schemas each with their own EXCEPT clause
+ALTER PUBLICATION testpub_alter_except SET TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.testpub_tbl_s1),
+                                                                      public EXCEPT (TABLE testpub_tbl1);
+\dRp+ testpub_alter_except
+
 -- fail: non-existing table in EXCEPT clause
 ALTER PUBLICATION testpub_alter_except DROP TABLES IN SCHEMA pub_test;
 ALTER PUBLICATION testpub_alter_except ADD TABLES IN SCHEMA pub_test EXCEPT (TABLE pub_test.nonexistent_table);
diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl
index 0ba6d6f8bb2..1308b6e43ed 100644
--- a/src/test/subscription/t/037_except.pl
+++ b/src/test/subscription/t/037_except.pl
@@ -376,6 +376,61 @@ $result =
 is($result, qq(0),
 	'ALTER ... ADD TABLES IN SCHEMA EXCEPT: excluded table not synced');
 
+# SET: replace the except list; tab_excluded is now included and tab_published is excluded.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION sch_pub SET TABLES IN SCHEMA sch1 EXCEPT (TABLE sch1.tab_published)"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION sch_sub REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sch_sub');
+
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO sch1.tab_published VALUES (7);
+	INSERT INTO sch1.tab_excluded VALUES (7);
+));
+$node_publisher->wait_for_catchup('sch_sub');
+
+$result =
+  $node_subscriber->safe_psql('postgres',
+	"SELECT a FROM sch1.tab_excluded WHERE a = 7");
+is($result, qq(7),
+	'ALTER ... SET TABLES IN SCHEMA EXCEPT: newly included table is replicated'
+);
+$result =
+  $node_subscriber->safe_psql('postgres',
+	"SELECT a FROM sch1.tab_published WHERE a = 7");
+is($result, qq(),
+	'ALTER ... SET TABLES IN SCHEMA EXCEPT: now-excluded table is not replicated'
+);
+
+# SET without EXCEPT: clears the except list; both tables are now published.
+$node_publisher->safe_psql('postgres',
+	"ALTER PUBLICATION sch_pub SET TABLES IN SCHEMA sch1");
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION sch_sub REFRESH PUBLICATION");
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sch_sub');
+
+$node_publisher->safe_psql(
+	'postgres', qq(
+	INSERT INTO sch1.tab_published VALUES (8);
+	INSERT INTO sch1.tab_excluded VALUES (8);
+));
+$node_publisher->wait_for_catchup('sch_sub');
+
+$result =
+  $node_subscriber->safe_psql('postgres',
+	"SELECT a FROM sch1.tab_published WHERE a = 8");
+is($result, qq(8),
+	'ALTER ... SET TABLES IN SCHEMA (no EXCEPT): tab_published replicated after except list cleared'
+);
+$result =
+  $node_subscriber->safe_psql('postgres',
+	"SELECT a FROM sch1.tab_excluded WHERE a = 8");
+is($result, qq(8),
+	'ALTER ... SET TABLES IN SCHEMA (no EXCEPT): tab_excluded replicated after except list cleared'
+);
+
 $node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sch_sub');
 $node_publisher->safe_psql('postgres', 'DROP PUBLICATION sch_pub');
 
@@ -443,6 +498,36 @@ $node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub');
 $node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1');
 $node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2');
 
+# OK when a table is excluded by a TABLES IN SCHEMA EXCEPT publication,
+# but is included by another publication.
+$node_publisher->safe_psql('postgres', 'TRUNCATE tab1');
+$node_subscriber->safe_psql('postgres', 'TRUNCATE tab1');
+
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE PUBLICATION tap_pub1 FOR TABLES IN SCHEMA public EXCEPT (TABLE public.tab1);
+	CREATE PUBLICATION tap_pub2 FOR TABLE tab1;
+	INSERT INTO tab1 VALUES(1);
+));
+$node_subscriber->psql('postgres',
+	"CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2"
+);
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub');
+
+$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2)));
+$node_publisher->wait_for_catchup('tap_sub');
+
+$result =
+  $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
+is( $result, qq(1
+2),
+	"TABLES IN SCHEMA EXCEPT: table excluded in schema pub but included by another pub is replicated"
+);
+
+$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub');
+$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1');
+$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2');
+
 $node_publisher->stop('fast');
 
 done_testing();
-- 
2.50.1 (Apple Git-155)