From 0c77fd5bb62715cd179ec26b61a0e9d782fe9d59 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Wed, 17 Dec 2025 15:39:23 +0800
Subject: [PATCH v2 2/2] =?UTF-8?q?Add=20INHERIT=20/=20NO=20INHERIT=20optio?=
 =?UTF-8?q?ns=20to=20ALTER=20TABLE=20=E2=80=A6=20REPLICA=20IDENTITY?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Extend ALTER TABLE … REPLICA IDENTITY with optional INHERIT and
NO INHERIT clauses for partitioned tables.

When INHERIT is specified, the replica identity change is propagated to
all existing partitions in the partition hierarchy, provided that each
partition already has the same replica identity setting as the parent.
If any partition differs, the command fails to avoid silently overriding
per-partition configuration. NO INHERIT (the default) changes only the
parent table and leaves existing partitions unchanged.

The INHERIT option is only allowed for partitioned tables and is not
supported for REPLICA IDENTITY USING INDEX, since index-based replica
identity is inherently per-table.

Documentation is updated to describe the new syntax and semantics, and
regression tests are added to cover valid propagation, error cases, and
default NO INHERIT behavior.

Author: Chao Li <lic@highgo.com>
Discussion: https://postgr.es/m/CAEoWx2nJ71hy8R614HQr7vQhkBReO9AANPODPg0aSQs74eOdLQ@mail.gmail.com
---
 doc/src/sgml/ref/alter_table.sgml             | 14 +++-
 src/backend/commands/tablecmds.c              | 42 +++++++++++
 src/backend/parser/gram.y                     | 17 ++++-
 src/include/nodes/parsenodes.h                |  2 +
 .../regress/expected/replica_identity.out     | 74 +++++++++++++++++++
 src/test/regress/sql/replica_identity.sql     | 46 ++++++++++++
 6 files changed, 191 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index fb9140c8794..ad2383b1776 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -93,7 +93,10 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     OF <replaceable class="parameter">type_name</replaceable>
     NOT OF
     OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
-    REPLICA IDENTITY { DEFAULT | USING INDEX <replaceable class="parameter">index_name</replaceable> | FULL | NOTHING }
+   REPLICA IDENTITY { DEFAULT [ INHERIT | NO INHERIT ] |
+                      USING INDEX <replaceable class="parameter">index_name</replaceable> |
+                      FULL [ INHERIT | NO INHERIT ] |
+                      NOTHING [ INHERIT | NO INHERIT ] }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -970,6 +973,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       attached with <command>ALTER TABLE ... ATTACH PARTITION</command> inherit
       that replica identity, though it can still be changed for a partition
       afterwards.
+     </para>
+     <para>
+      The optional <literal>INHERIT</literal> clause (only valid
+      for partitioned tables) applies the
+      change to all existing partitions that already match the parent's replica
+      identity; if any partition has a different replica identity, the command
+      fails.  <literal>NO INHERIT</literal> (the default) leaves existing
+      partitions unchanged.  Replica identity set via <literal>USING
+      INDEX</literal> cannot specify inheritance.
      <variablelist>
       <varlistentry id="sql-altertable-replica-identity-default">
        <term><literal>DEFAULT</literal></term>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..54413f65a8e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -18519,6 +18519,48 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	Relation	indexRel;
 	int			key;
 
+	if (stmt->inherit)
+	{
+		List	   *partRelIds = NIL;
+
+		/* Assert that we're not trying to set an index-based replica identity */
+		Assert(stmt->identity_type != REPLICA_IDENTITY_INDEX);
+
+		if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("inherit option is only valid for partitioned tables")));
+
+		partRelIds = GetPubPartitionOptionRelations(partRelIds, PUBLICATION_PART_ALL,
+													RelationGetRelid(rel));
+		foreach_oid(partRelOid, partRelIds)
+		{
+			Relation	partRel;
+
+			/*
+			 * Because we used PUBLICATION_PART_ALL, partRelIds contains OIDs
+			 * of the entire parition tree, so we need to skip the parent
+			 * table itself.
+			 */
+			if (partRelOid == RelationGetRelid(rel))
+				continue;
+
+			partRel = relation_open(partRelOid, ExclusiveLock);
+
+			/* Check if the partition has a different replica identity setting */
+			if (partRel->rd_rel->relreplident != rel->rd_rel->relreplident)
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("partition %s has a different replica identity setting",
+								quote_identifier(RelationGetRelationName(partRel)))));
+			}
+
+			relation_mark_replica_identity(partRel, stmt->identity_type, InvalidOid, true);
+			relation_close(partRel, ExclusiveLock);
+		}
+	}
+
 	if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT)
 	{
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..aa2e21a27eb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -337,6 +337,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	alter_table_cmd alter_type_cmd opt_collate_clause
 	   replica_identity partition_cmd index_partition_cmd
+%type <boolean> opt_replica_identity_inherit
 %type <list>	alter_table_cmds alter_type_cmds
 %type <list>    alter_identity_column_option_list
 %type <defelt>  alter_identity_column_option
@@ -3147,27 +3148,30 @@ alter_using:
 		;
 
 replica_identity:
-			NOTHING
+			NOTHING opt_replica_identity_inherit
 				{
 					ReplicaIdentityStmt *n = makeNode(ReplicaIdentityStmt);
 
 					n->identity_type = REPLICA_IDENTITY_NOTHING;
+					n->inherit = $2;
 					n->name = NULL;
 					$$ = (Node *) n;
 				}
-			| FULL
+			| FULL opt_replica_identity_inherit
 				{
 					ReplicaIdentityStmt *n = makeNode(ReplicaIdentityStmt);
 
 					n->identity_type = REPLICA_IDENTITY_FULL;
+					n->inherit = $2;
 					n->name = NULL;
 					$$ = (Node *) n;
 				}
-			| DEFAULT
+			| DEFAULT opt_replica_identity_inherit
 				{
 					ReplicaIdentityStmt *n = makeNode(ReplicaIdentityStmt);
 
 					n->identity_type = REPLICA_IDENTITY_DEFAULT;
+					n->inherit = $2;
 					n->name = NULL;
 					$$ = (Node *) n;
 				}
@@ -3176,11 +3180,18 @@ replica_identity:
 					ReplicaIdentityStmt *n = makeNode(ReplicaIdentityStmt);
 
 					n->identity_type = REPLICA_IDENTITY_INDEX;
+					n->inherit = false;
 					n->name = $3;
 					$$ = (Node *) n;
 				}
 ;
 
+opt_replica_identity_inherit:
+			/* EMPTY */					{ $$ = false; }
+			| INHERIT					{ $$ = true; }
+			| NO INHERIT				{ $$ = false; }
+		;
+
 reloptions:
 			'(' reloption_list ')'					{ $$ = $2; }
 		;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..a569e7d3754 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2545,6 +2545,8 @@ typedef struct ReplicaIdentityStmt
 {
 	NodeTag		type;
 	char		identity_type;
+	bool		inherit;		/* apply to child tables, only valid for
+								 * identity_type 'd', 'f' and 'n' */
 	char	   *name;
 } ReplicaIdentityStmt;
 
diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out
index 9766bd5af89..59ec371d205 100644
--- a/src/test/regress/expected/replica_identity.out
+++ b/src/test/regress/expected/replica_identity.out
@@ -357,6 +357,76 @@ SELECT relreplident FROM pg_class
  f
 (1 row)
 
+-- INHERIT option is only valid for partitioned tables
+CREATE TABLE test_replica_identity8 (a int);
+ALTER TABLE test_replica_identity8 REPLICA IDENTITY FULL INHERIT;
+ERROR:  inherit option is only valid for partitioned tables
+-- INHERIT propagates replica identity change to partitions when they match
+CREATE TABLE test_replica_identity9 (a int) PARTITION BY LIST (a);
+CREATE TABLE test_replica_identity9_p1 PARTITION OF test_replica_identity9
+  FOR VALUES IN (1);
+CREATE TABLE test_replica_identity9_p2 PARTITION OF test_replica_identity9
+  FOR VALUES IN (2);
+ALTER TABLE test_replica_identity9 REPLICA IDENTITY FULL INHERIT;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity9'::regclass;
+ relreplident 
+--------------
+ f
+(1 row)
+
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity9_p1'::regclass;
+ relreplident 
+--------------
+ f
+(1 row)
+
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity9_p2'::regclass;
+ relreplident 
+--------------
+ f
+(1 row)
+
+-- INHERIT fails if a partition already has a different replica identity
+CREATE TABLE test_replica_identity10 (a int) PARTITION BY LIST (a);
+CREATE TABLE test_replica_identity10_p1 PARTITION OF test_replica_identity10
+  FOR VALUES IN (1);
+CREATE TABLE test_replica_identity10_p2 PARTITION OF test_replica_identity10
+  FOR VALUES IN (2);
+ALTER TABLE test_replica_identity10_p2 REPLICA IDENTITY FULL;
+ALTER TABLE test_replica_identity10 REPLICA IDENTITY NOTHING INHERIT;
+ERROR:  partition test_replica_identity10_p2 has a different replica identity setting
+-- NO INHERIT changes only the parent replica identity
+CREATE TABLE test_replica_identity11 (a int) PARTITION BY LIST (a);
+CREATE TABLE test_replica_identity11_p1 PARTITION OF test_replica_identity11
+  FOR VALUES IN (1);
+CREATE TABLE test_replica_identity11_p2 PARTITION OF test_replica_identity11
+  FOR VALUES IN (2);
+ALTER TABLE test_replica_identity11_p1 REPLICA IDENTITY FULL;
+ALTER TABLE test_replica_identity11 REPLICA IDENTITY NOTHING NO INHERIT;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity11'::regclass;
+ relreplident 
+--------------
+ n
+(1 row)
+
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity11_p1'::regclass;
+ relreplident 
+--------------
+ f
+(1 row)
+
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity11_p2'::regclass;
+ relreplident 
+--------------
+ d
+(1 row)
+
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity2;
 DROP TABLE test_replica_identity3;
@@ -366,3 +436,7 @@ DROP TABLE test_replica_identity_othertable;
 DROP TABLE test_replica_identity_t3;
 DROP TABLE test_replica_identity6;
 DROP TABLE test_replica_identity7;
+DROP TABLE test_replica_identity8;
+DROP TABLE test_replica_identity9;
+DROP TABLE test_replica_identity10;
+DROP TABLE test_replica_identity11;
diff --git a/src/test/regress/sql/replica_identity.sql b/src/test/regress/sql/replica_identity.sql
index 7c0c7365c26..77038f8480f 100644
--- a/src/test/regress/sql/replica_identity.sql
+++ b/src/test/regress/sql/replica_identity.sql
@@ -173,6 +173,48 @@ ALTER TABLE test_replica_identity7_p ATTACH PARTITION test_replica_identity7_lea
 SELECT relreplident FROM pg_class
   WHERE oid = 'test_replica_identity7_leaf'::regclass;
 
+-- INHERIT option is only valid for partitioned tables
+CREATE TABLE test_replica_identity8 (a int);
+ALTER TABLE test_replica_identity8 REPLICA IDENTITY FULL INHERIT;
+
+-- INHERIT propagates replica identity change to partitions when they match
+CREATE TABLE test_replica_identity9 (a int) PARTITION BY LIST (a);
+CREATE TABLE test_replica_identity9_p1 PARTITION OF test_replica_identity9
+  FOR VALUES IN (1);
+CREATE TABLE test_replica_identity9_p2 PARTITION OF test_replica_identity9
+  FOR VALUES IN (2);
+ALTER TABLE test_replica_identity9 REPLICA IDENTITY FULL INHERIT;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity9'::regclass;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity9_p1'::regclass;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity9_p2'::regclass;
+
+-- INHERIT fails if a partition already has a different replica identity
+CREATE TABLE test_replica_identity10 (a int) PARTITION BY LIST (a);
+CREATE TABLE test_replica_identity10_p1 PARTITION OF test_replica_identity10
+  FOR VALUES IN (1);
+CREATE TABLE test_replica_identity10_p2 PARTITION OF test_replica_identity10
+  FOR VALUES IN (2);
+ALTER TABLE test_replica_identity10_p2 REPLICA IDENTITY FULL;
+ALTER TABLE test_replica_identity10 REPLICA IDENTITY NOTHING INHERIT;
+
+-- NO INHERIT changes only the parent replica identity
+CREATE TABLE test_replica_identity11 (a int) PARTITION BY LIST (a);
+CREATE TABLE test_replica_identity11_p1 PARTITION OF test_replica_identity11
+  FOR VALUES IN (1);
+CREATE TABLE test_replica_identity11_p2 PARTITION OF test_replica_identity11
+  FOR VALUES IN (2);
+ALTER TABLE test_replica_identity11_p1 REPLICA IDENTITY FULL;
+ALTER TABLE test_replica_identity11 REPLICA IDENTITY NOTHING NO INHERIT;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity11'::regclass;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity11_p1'::regclass;
+SELECT relreplident FROM pg_class
+  WHERE oid = 'test_replica_identity11_p2'::regclass;
+
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity2;
 DROP TABLE test_replica_identity3;
@@ -182,3 +224,7 @@ DROP TABLE test_replica_identity_othertable;
 DROP TABLE test_replica_identity_t3;
 DROP TABLE test_replica_identity6;
 DROP TABLE test_replica_identity7;
+DROP TABLE test_replica_identity8;
+DROP TABLE test_replica_identity9;
+DROP TABLE test_replica_identity10;
+DROP TABLE test_replica_identity11;
-- 
2.39.5 (Apple Git-154)

