From 6bdac5a2fe158c5ba612c0cff742a9a2d54c060a Mon Sep 17 00:00:00 2001
From: Peter Smith <peter.b.smith@fujitsu.com>
Date: Fri, 28 Aug 2026 13:11:04 +1000
Subject: [PATCH v1] Error if same table has ONLY mismatch.

Patch so any clashing ONLY-ness of the same parent table gives ERROR:

CREATE PUBLICATION pub FOR TABLE parent, TABLE ONLY parent; -- ERROR, conflict
CREATE PUBLICATION pub FOR TABLE ONLY parent, TABLE parent; -- ERROR, conflict

~

ONLY has no effect for partitioned tables, so clashing ONLY-ness of same
partitioned table is ignored, same as before:

CREATE PUBLICATION pub FOR TABLE root, TABLE ONLY root; -- OK
CREATE PUBLICATION pub FOR TABLE ONLY root, TABLE root; -- OK

~

In passing, the docs for partitioned tables and ONLY is fixed to remove the
ambiguity of the previous wording.

Author: Peter Smith <smithpb2250@gmail.com>
---
 doc/src/sgml/ref/create_publication.sgml  |  6 +--
 src/backend/commands/publicationcmds.c    | 20 +++++++++
 src/test/regress/expected/publication.out | 51 ++++++++++++++++++++++-
 src/test/regress/sql/publication.sql      | 23 +++++++++-
 4 files changed, 95 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 35c28006f60..34f08263183 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -119,9 +119,9 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
       Optionally, <literal>*</literal> can be specified after the
       <replaceable class="parameter">table_name</replaceable> to
       explicitly indicate that descendant tables are included.
-      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 added to the publication.
+      For a partitioned table, <literal>ONLY</literal> and <literal>*</literal>
+      have no effect: its partitions are always implicitly considered part of
+      the publication, so they are never explicitly added to the publication.
      </para>
 
      <para>
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 96838730fe1..0a11a49886c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1842,6 +1842,7 @@ OpenTableList(List *tables)
 	ListCell   *lc;
 	List	   *relids_with_rf = NIL;
 	List	   *relids_with_collist = NIL;
+	List	   *relids_recursing = NIL;
 
 	/*
 	 * Open, share-lock, and check all the explicitly-specified relations
@@ -1883,6 +1884,22 @@ OpenTableList(List *tables)
 						 errmsg("conflicting or redundant column lists for table \"%s\"",
 								RelationGetRelationName(rel))));
 
+			/*
+			 * Disallow duplicate tables if one mention specifies ONLY and
+			 * another does not, since it's not clear whether descendant
+			 * tables should be included.  Plain "foo, foo" (agreeing on
+			 * ONLY-ness) remains a silently-tolerated no-op, as before.
+			 *
+			 * Mismatching ONLY-ness remains silently-tolerated for
+			 * partitioned tables since ONLY has no meaning for them anyway.
+			 */
+			if (recurse != list_member_oid(relids_recursing, myrelid) &&
+				rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+						 errmsg("conflicting ONLY specifications for table \"%s\"",
+								RelationGetRelationName(rel))));
+
 			table_close(rel, ShareUpdateExclusiveLock);
 			continue;
 		}
@@ -1901,6 +1918,9 @@ OpenTableList(List *tables)
 		if (t->columns)
 			relids_with_collist = lappend_oid(relids_with_collist, myrelid);
 
+		if (recurse)
+			relids_recursing = lappend_oid(relids_recursing, myrelid);
+
 		/*
 		 * Add children of this rel, if requested, so that they too are added
 		 * to the publication.  A partitioned table can't have any inheritance
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4f21462cc17..b3eac6f60b1 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -429,9 +429,38 @@ CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT (TABLE ONLY testpub_tbl_parent
 Except tables:
     "public.testpub_tbl_parent"
 
+-- Listing the same table twice is fine as long as the mentions agree on
+-- ONLY-ness.
+CREATE PUBLICATION testpub_dup1 FOR TABLE testpub_tbl_parent, TABLE testpub_tbl_parent;
+\dRp+ testpub_dup1
+                                                           Publication testpub_dup1
+          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:
+    "public.testpub_tbl_child"
+    "public.testpub_tbl_parent"
+
+CREATE PUBLICATION testpub_dup2 FOR TABLE ONLY testpub_tbl_parent, TABLE ONLY testpub_tbl_parent;
+\dRp+ testpub_dup2
+                                                           Publication testpub_dup2
+          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:
+    "public.testpub_tbl_parent"
+
+-- But mixing ONLY and non-ONLY mentions of the same table is ambiguous
+-- (would descendants be included or not?) and must be rejected.
 RESET client_min_messages;
+CREATE PUBLICATION testpub_conflict1 FOR TABLE testpub_tbl_parent, TABLE ONLY testpub_tbl_parent;
+ERROR:  conflicting ONLY specifications for table "testpub_tbl_parent"
+CREATE PUBLICATION testpub_conflict2 FOR TABLE ONLY testpub_tbl_parent, TABLE testpub_tbl_parent;
+ERROR:  conflicting ONLY specifications for table "testpub_tbl_parent"
+SET client_min_messages = 'ERROR';
 DROP TABLE testpub_tbl_parent, testpub_tbl_child;
 DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
+DROP PUBLICATION testpub_dup1, testpub_dup2;
 ---------------------------------------------
 -- EXCEPT clause tests for partitioned tables
 ---------------------------------------------
@@ -606,8 +635,28 @@ HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
 ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted;
 -- works again, because update is no longer replicated
 UPDATE testpub_parted2 SET a = 2;
+-- Mixing ONLY and non-ONLY mentions of the same partitioned table is silently
+-- tolerated because ONLY has no effect for partitioned tables anyway.
+CREATE PUBLICATION testpub_dup1 FOR TABLE testpub_parted, TABLE ONLY testpub_parted;
+\dRp+ testpub_dup1
+                                                           Publication testpub_dup1
+          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:
+    "public.testpub_parted"
+
+CREATE PUBLICATION testpub_dup2 FOR TABLE ONLY testpub_parted, TABLE testpub_parted;
+\dRp+ testpub_dup2
+                                                           Publication testpub_dup2
+          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:
+    "public.testpub_parted"
+
 DROP TABLE testpub_parted1, testpub_parted2;
-DROP PUBLICATION testpub_forparted, testpub_forparted1;
+DROP PUBLICATION testpub_forparted, testpub_forparted1, testpub_dup1, testpub_dup2;
 -- Tests for row filters
 CREATE TABLE testpub_rf_tbl1 (a integer, b text);
 CREATE TABLE testpub_rf_tbl2 (c text, d integer);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index fac54b02e27..b3a1887c699 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -197,9 +197,23 @@ CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent *);
 CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT (TABLE ONLY testpub_tbl_parent);
 \dRp+ testpub7
 
+-- Listing the same table twice is fine as long as the mentions agree on
+-- ONLY-ness.
+CREATE PUBLICATION testpub_dup1 FOR TABLE testpub_tbl_parent, TABLE testpub_tbl_parent;
+\dRp+ testpub_dup1
+CREATE PUBLICATION testpub_dup2 FOR TABLE ONLY testpub_tbl_parent, TABLE ONLY testpub_tbl_parent;
+\dRp+ testpub_dup2
+
+-- But mixing ONLY and non-ONLY mentions of the same table is ambiguous
+-- (would descendants be included or not?) and must be rejected.
 RESET client_min_messages;
+CREATE PUBLICATION testpub_conflict1 FOR TABLE testpub_tbl_parent, TABLE ONLY testpub_tbl_parent;
+CREATE PUBLICATION testpub_conflict2 FOR TABLE ONLY testpub_tbl_parent, TABLE testpub_tbl_parent;
+
+SET client_min_messages = 'ERROR';
 DROP TABLE testpub_tbl_parent, testpub_tbl_child;
 DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
+DROP PUBLICATION testpub_dup1, testpub_dup2;
 
 ---------------------------------------------
 -- EXCEPT clause tests for partitioned tables
@@ -295,8 +309,15 @@ UPDATE testpub_parted2 SET a = 2;
 ALTER PUBLICATION testpub_forparted DROP TABLE testpub_parted;
 -- works again, because update is no longer replicated
 UPDATE testpub_parted2 SET a = 2;
+-- Mixing ONLY and non-ONLY mentions of the same partitioned table is silently
+-- tolerated because ONLY has no effect for partitioned tables anyway.
+CREATE PUBLICATION testpub_dup1 FOR TABLE testpub_parted, TABLE ONLY testpub_parted;
+\dRp+ testpub_dup1
+CREATE PUBLICATION testpub_dup2 FOR TABLE ONLY testpub_parted, TABLE testpub_parted;
+\dRp+ testpub_dup2
+
 DROP TABLE testpub_parted1, testpub_parted2;
-DROP PUBLICATION testpub_forparted, testpub_forparted1;
+DROP PUBLICATION testpub_forparted, testpub_forparted1, testpub_dup1, testpub_dup2;
 
 -- Tests for row filters
 CREATE TABLE testpub_rf_tbl1 (a integer, b text);
-- 
2.47.3

