From 0a6a7201a2cbb396089ded5af33c51a57930824b Mon Sep 17 00:00:00 2001
From: Arunprasad Rajkumar <ar.arunprasad@gmail.com>
Date: Fri, 12 Dec 2025 19:43:38 +0530
Subject: [PATCH] Skip unpublishable child tables when adding parent to
 publication

When adding a parent table with INHERITS children to a publication,
automatically skip child tables that cannot be replicated (foreign
tables, temporary tables, and unlogged tables) instead of failing
with an error.

Previously, attempting to add a parent table to a publication would
fail if any of its inherited children were foreign tables, temporary
tables, or unlogged tables, since these relation types cannot be
replicated. This required users to manually manage which tables to
include, making inheritance hierarchies difficult to work with in
logical replication scenarios.

This change modifies OpenTableList() to check each child relation's
publishability using is_publishable_relation() before adding it to
the publication. Unpublishable children are skipped with a NOTICE
message indicating the relation type and reason, while the parent
and any publishable children are successfully added.

The NOTICE messages follow the same format as VACUUM and ANALYZE
commands, with a main message and detailed explanation:
  NOTICE: skipping "table_name" --- cannot add relation to publication
  DETAIL: Foreign tables cannot be replicated.

This is a backward-compatible change that relaxes restrictions.
Operations that previously failed will now succeed with appropriate
user notifications via NOTICE messages.
---
 src/backend/commands/publicationcmds.c    | 38 ++++++++++++++++++++
 src/test/regress/expected/publication.out | 43 +++++++++++++++++++++++
 src/test/regress/sql/publication.sql      | 28 +++++++++++++++
 3 files changed, 109 insertions(+)

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index a1983508950..a3fac492d25 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1826,6 +1826,44 @@ OpenTableList(List *tables)
 
 				/* find_all_inheritors already got lock */
 				rel = table_open(childrelid, NoLock);
+
+				/*
+				 * Skip child relations that cannot be replicated. When adding
+				 * a parent table with INHERITS children to a publication, we
+				 * want to include only regular tables and partitioned tables.
+				 * Foreign tables and temporary/unlogged tables cannot be
+				 * replicated.
+				 *
+				 * Note: Views, materialized views, and sequences cannot use
+				 * INHERITS, so find_all_inheritors() will only return tables
+				 * and foreign tables.
+				 */
+				if (!is_publishable_relation(rel))
+				{
+					if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+						ereport(NOTICE,
+								(errmsg("skipping \"%s\" --- cannot add relation to publication",
+										RelationGetRelationName(rel)),
+								 errdetail("Foreign tables cannot be replicated.")));
+					else if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+						ereport(NOTICE,
+								(errmsg("skipping \"%s\" --- cannot add relation to publication",
+										RelationGetRelationName(rel)),
+								 errdetail("Temporary tables cannot be replicated.")));
+					else if (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED)
+						ereport(NOTICE,
+								(errmsg("skipping \"%s\" --- cannot add relation to publication",
+										RelationGetRelationName(rel)),
+								 errdetail("Unlogged tables cannot be replicated.")));
+					else
+						ereport(NOTICE,
+								(errmsg("skipping \"%s\" --- cannot add relation to publication",
+										RelationGetRelationName(rel))));
+
+					table_close(rel, NoLock);
+					continue;
+				}
+
 				pub_rel = palloc_object(PublicationRelInfo);
 				pub_rel->relation = rel;
 				/* child inherits WHERE clause from parent */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index e72d1308967..b34f936f360 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -240,6 +240,49 @@ Tables:
 
 DROP TABLE testpub_tbl3, testpub_tbl3a;
 DROP PUBLICATION testpub3, testpub4;
+-- Test skipping unpublishable child tables (foreign, temp, unlogged)
+CREATE FOREIGN DATA WRAPPER dummy_fdw;
+CREATE SERVER dummy_server FOREIGN DATA WRAPPER dummy_fdw;
+CREATE TABLE testpub_parent_skip (a int);
+CREATE TABLE testpub_child_regular (a int) INHERITS (testpub_parent_skip);
+NOTICE:  merging column "a" with inherited definition
+CREATE FOREIGN TABLE testpub_child_foreign (a int) INHERITS (testpub_parent_skip)
+    SERVER dummy_server;
+NOTICE:  merging column "a" with inherited definition
+CREATE TEMP TABLE testpub_child_temp (a int) INHERITS (testpub_parent_skip);
+NOTICE:  merging column "a" with inherited definition
+CREATE UNLOGGED TABLE testpub_child_unlogged (a int) INHERITS (testpub_parent_skip);
+NOTICE:  merging column "a" with inherited definition
+-- Should skip foreign, temp, and unlogged children with NOTICE
+SET client_min_messages = 'NOTICE';
+CREATE PUBLICATION testpub_skip_child_pub FOR TABLE testpub_parent_skip;
+NOTICE:  skipping "testpub_child_foreign" --- cannot add relation to publication
+DETAIL:  Foreign tables cannot be replicated.
+NOTICE:  skipping "testpub_child_temp" --- cannot add relation to publication
+DETAIL:  Temporary tables cannot be replicated.
+NOTICE:  skipping "testpub_child_unlogged" --- cannot add relation to publication
+DETAIL:  Unlogged tables cannot be replicated.
+WARNING:  "wal_level" is insufficient to publish logical changes
+HINT:  Set "wal_level" to "logical" before creating subscriptions.
+RESET client_min_messages;
+-- Verify only parent and regular child are in publication
+SELECT * FROM pg_publication_tables
+WHERE pubname = 'testpub_skip_child_pub'
+ORDER BY tablename;
+        pubname         | schemaname |       tablename       | attnames | rowfilter 
+------------------------+------------+-----------------------+----------+-----------
+ testpub_skip_child_pub | public     | testpub_child_regular | {a}      | 
+ testpub_skip_child_pub | public     | testpub_parent_skip   | {a}      | 
+(2 rows)
+
+DROP PUBLICATION testpub_skip_child_pub;
+DROP TABLE testpub_child_unlogged;
+DROP FOREIGN TABLE testpub_child_foreign;
+DROP TABLE testpub_child_regular;
+DROP TABLE testpub_parent_skip CASCADE;
+NOTICE:  drop cascades to table testpub_child_temp
+DROP SERVER dummy_server;
+DROP FOREIGN DATA WRAPPER dummy_fdw;
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 00390aecd47..c0fc2b687b6 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -120,6 +120,34 @@ RESET client_min_messages;
 DROP TABLE testpub_tbl3, testpub_tbl3a;
 DROP PUBLICATION testpub3, testpub4;
 
+-- Test skipping unpublishable child tables (foreign, temp, unlogged)
+CREATE FOREIGN DATA WRAPPER dummy_fdw;
+CREATE SERVER dummy_server FOREIGN DATA WRAPPER dummy_fdw;
+CREATE TABLE testpub_parent_skip (a int);
+CREATE TABLE testpub_child_regular (a int) INHERITS (testpub_parent_skip);
+CREATE FOREIGN TABLE testpub_child_foreign (a int) INHERITS (testpub_parent_skip)
+    SERVER dummy_server;
+CREATE TEMP TABLE testpub_child_temp (a int) INHERITS (testpub_parent_skip);
+CREATE UNLOGGED TABLE testpub_child_unlogged (a int) INHERITS (testpub_parent_skip);
+
+-- Should skip foreign, temp, and unlogged children with NOTICE
+SET client_min_messages = 'NOTICE';
+CREATE PUBLICATION testpub_skip_child_pub FOR TABLE testpub_parent_skip;
+RESET client_min_messages;
+
+-- Verify only parent and regular child are in publication
+SELECT * FROM pg_publication_tables
+WHERE pubname = 'testpub_skip_child_pub'
+ORDER BY tablename;
+
+DROP PUBLICATION testpub_skip_child_pub;
+DROP TABLE testpub_child_unlogged;
+DROP FOREIGN TABLE testpub_child_foreign;
+DROP TABLE testpub_child_regular;
+DROP TABLE testpub_parent_skip CASCADE;
+DROP SERVER dummy_server;
+DROP FOREIGN DATA WRAPPER dummy_fdw;
+
 --- Tests for publications with SEQUENCES
 CREATE SEQUENCE regress_pub_seq0;
 CREATE SEQUENCE pub_test.regress_pub_seq1;
-- 
2.39.5 (Apple Git-154)

