From 84bc1e645486e4c662e73163a063e0d804b989cd Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Tue, 28 Jul 2026 13:53:22 +0800
Subject: [PATCH v10 2/2] tablecmds: preserve partition index properties during
 ALTER TYPE

Record replica identity and clustered indexes across partition
hierarchies before rebuilding dependent indexes. Restore each
marking on the owning table after the indexes have been recreated.

Reported-by: Sami Imseih <samimseih@gmail.com>
Reported-by: Chao Li <lic@highgo.com>
Author: Chao Li <lic@highgo.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Xuneng Zhou <xunengzhou@gmail.com>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Alberto Piai <alberto.piai@gmail.com>
Discussion: https://postgr.es/m/DB533C25-C6BA-4C0F-8046-96168E9CDD72@gmail.com
---
 src/backend/commands/tablecmds.c              | 196 +++++++++++++-----
 src/test/regress/expected/alter_table.out     |  19 +-
 .../regress/expected/replica_identity.out     | 107 ++++++++++
 src/test/regress/sql/alter_table.sql          |  10 +-
 src/test/regress/sql/replica_identity.sql     |  76 +++++++
 5 files changed, 343 insertions(+), 65 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bbb42e3c3b0..89e660c5857 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -207,8 +207,9 @@ typedef struct AlteredTableInfo
 	List	   *changedConstraintDefs;	/* string definitions of same */
 	List	   *changedIndexOids;	/* OIDs of indexes to rebuild */
 	List	   *changedIndexDefs;	/* string definitions of same */
-	char	   *replicaIdentityIndex;	/* index to reset as REPLICA IDENTITY */
-	char	   *clusterOnIndex; /* index to use for CLUSTER */
+	List	   *replicaIdentityIndexOids;	/* OIDs of index to reset as
+											 * REPLICA IDENTITY */
+	List	   *clusterOnIndexOids;	/* OIDs of indexes to use for CLUSTER */
 	List	   *changedStatisticsOids;	/* OIDs of statistics to rebuild */
 	List	   *changedStatisticsDefs;	/* string definitions of same */
 } AlteredTableInfo;
@@ -692,10 +693,10 @@ static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
 static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 										   AlterTableCmd *cmd, LOCKMODE lockmode);
 static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
-											  Relation rel, AttrNumber attnum, const char *colName);
-static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel);
-static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab);
-static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab);
+											  Relation rel, AttrNumber attnum, const char *colName, LOCKMODE lockmode);
+static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel, LOCKMODE lockmode);
+static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab, LOCKMODE lockmode);
+static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab, LOCKMODE lockmode);
 static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
 static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab,
 								   LOCKMODE lockmode);
@@ -8818,14 +8819,14 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * Find everything that depends on the column (constraints, indexes, etc),
 	 * and record enough information to let us recreate the objects.
 	 */
-	RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+	RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName, lockmode);
 
 	/*
 	 * Find whole-row referenced objects that depend on the column
 	 * (constraints, indexes, etc.), and record enough information to let us
 	 * recreate the objects.
 	 */
-	RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel);
+	RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel, lockmode);
 
 	/*
 	 * Drop the dependency records of the GENERATED expression, in particular
@@ -15378,7 +15379,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 	 * the info before executing ALTER TYPE, though, else the deparser will
 	 * get confused.
 	 */
-	RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName);
+	RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName, lockmode);
 
 	/*
 	 * Now scan for dependencies of this column on other things.  The only
@@ -15581,7 +15582,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
  */
 static void
 RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
-								  Relation rel, AttrNumber attnum, const char *colName)
+								  Relation rel, AttrNumber attnum, const char *colName, LOCKMODE lockmode)
 {
 	Relation	depRel;
 	ScanKeyData key[3];
@@ -15627,7 +15628,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 						relKind == RELKIND_PARTITIONED_INDEX)
 					{
 						Assert(foundObject.objectSubId == 0);
-						RememberIndexForRebuilding(foundObject.objectId, tab);
+						RememberIndexForRebuilding(foundObject.objectId, tab, lockmode);
 					}
 					else if (relKind == RELKIND_SEQUENCE)
 					{
@@ -15648,7 +15649,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 
 			case ConstraintRelationId:
 				Assert(foundObject.objectSubId == 0);
-				RememberConstraintForRebuilding(foundObject.objectId, tab);
+				RememberConstraintForRebuilding(foundObject.objectId, tab, lockmode);
 				break;
 
 			case ProcedureRelationId:
@@ -15801,6 +15802,46 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 	table_close(depRel, NoLock);
 }
 
+static void
+find_partition_index_properties(AlteredTableInfo *tab, Oid relId,
+								Oid indexId, LOCKMODE lockmode)
+{
+	List	   *partRelIds = NIL;
+
+	partRelIds = find_inheritance_children(relId, lockmode);
+	foreach_oid(partRelOid, partRelIds)
+	{
+		Relation	partRel;
+		Oid			partIndexId;
+
+		/* find_inheritance_children already got lock */
+		partRel = relation_open(partRelOid, NoLock);
+
+		partIndexId = index_get_partition(partRel, indexId);
+		if (!OidIsValid(partIndexId))
+		{
+			/* if we won't touch the partition, then unlock it */
+			relation_close(partRel, lockmode);
+			continue;
+		}
+
+		if (get_index_isreplident(partIndexId))
+			tab->replicaIdentityIndexOids = lappend_oid(tab->replicaIdentityIndexOids, partIndexId);
+		if (get_index_isclustered(partIndexId))
+			tab->clusterOnIndexOids = lappend_oid(tab->clusterOnIndexOids,
+												 partIndexId);
+
+		if (partRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		{
+			find_partition_index_properties(tab, partRelOid, partIndexId,
+											lockmode);
+		}
+
+		relation_close(partRel, NoLock);
+	}
+	list_free(partRelIds);
+}
+
 /*
  * Record information about dependencies between objects with whole-row Var
  * references (indexes, check constraints, etc.) and the relation.
@@ -15811,7 +15852,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
  * This function currently applies only to ALTER COLUMN SET EXPRESSION.
  */
 static void
-RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel)
+RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel, LOCKMODE lockmode)
 {
 	ScanKeyData skey;
 	Relation	pg_constraint;
@@ -15878,7 +15919,7 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 				 */
 				if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
 				{
-					RememberConstraintForRebuilding(conform->oid, tab);
+					RememberConstraintForRebuilding(conform->oid, tab, lockmode);
 				}
 			}
 		}
@@ -15932,7 +15973,7 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 			 */
 			if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
 			{
-				RememberIndexForRebuilding(index->indexrelid, tab);
+				RememberIndexForRebuilding(index->indexrelid, tab, lockmode);
 				continue;
 			}
 		}
@@ -15960,7 +16001,7 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 			 */
 			if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
 			{
-				RememberIndexForRebuilding(index->indexrelid, tab);
+				RememberIndexForRebuilding(index->indexrelid, tab, lockmode);
 			}
 		}
 	}
@@ -15976,13 +16017,16 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 static void
 RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab)
 {
-	if (!get_index_isreplident(indoid))
-		return;
-
-	if (tab->replicaIdentityIndex)
-		elog(ERROR, "relation %u has multiple indexes marked as replica identity", tab->relid);
+	if (get_index_isreplident(indoid))
+	{
+		foreach_oid(indexId, tab->replicaIdentityIndexOids)
+		{
+			if (IndexGetRelation(indexId, false) == tab->relid)
+				elog(ERROR, "relation %u has multiple indexes marked as replica identity", tab->relid);
+		}
 
-	tab->replicaIdentityIndex = get_rel_name(indoid);
+		tab->replicaIdentityIndexOids = lappend_oid(tab->replicaIdentityIndexOids, indoid);
+	}
 }
 
 /*
@@ -15991,13 +16035,40 @@ RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab)
 static void
 RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab)
 {
+	Oid			relid;
+
 	if (!get_index_isclustered(indoid))
 		return;
 
-	if (tab->clusterOnIndex)
-		elog(ERROR, "relation %u has multiple clustered indexes", tab->relid);
+	relid = IndexGetRelation(indoid, false);
+	foreach_oid(indexId, tab->clusterOnIndexOids)
+	{
+		if (IndexGetRelation(indexId, false) == relid)
+			elog(ERROR, "relation %u has multiple clustered indexes", relid);
+	}
 
-	tab->clusterOnIndex = get_rel_name(indoid);
+	tab->clusterOnIndexOids = lappend_oid(tab->clusterOnIndexOids, indoid);
+}
+
+/*
+ * Remember the index properties that need to be restored after rebuilding.
+ */
+static void
+RememberIndexPropertiesForRebuilding(Oid indoid, AlteredTableInfo *tab,
+									 LOCKMODE lockmode)
+{
+	RememberReplicaIdentityForRebuilding(indoid, tab);
+	RememberClusterOnForRebuilding(indoid, tab);
+
+	/* For regular tables, there are no partition indexes to inspect. */
+	if (tab->rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		return;
+
+	/*
+	 * A partition may select its own replica identity and clustered indexes,
+	 * so inspect the whole partition index hierarchy before it is dropped.
+	 */
+	find_partition_index_properties(tab, tab->relid, indoid, lockmode);
 }
 
 /*
@@ -16005,7 +16076,7 @@ RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab)
  * to be rebuilt (which we might already know).
  */
 static void
-RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab)
+RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab, LOCKMODE lockmode)
 {
 	/*
 	 * This de-duplication check is critical for two independent reasons: we
@@ -16049,10 +16120,7 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab)
 		 */
 		indoid = get_constraint_index(conoid);
 		if (OidIsValid(indoid))
-		{
-			RememberReplicaIdentityForRebuilding(indoid, tab);
-			RememberClusterOnForRebuilding(indoid, tab);
-		}
+			RememberIndexPropertiesForRebuilding(indoid, tab, lockmode);
 	}
 }
 
@@ -16061,7 +16129,7 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab)
  * to be rebuilt (which we might already know).
  */
 static void
-RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab)
+RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab, LOCKMODE lockmode)
 {
 	/*
 	 * This de-duplication check is critical for two independent reasons: we
@@ -16084,7 +16152,7 @@ RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab)
 
 		if (OidIsValid(conoid))
 		{
-			RememberConstraintForRebuilding(conoid, tab);
+			RememberConstraintForRebuilding(conoid, tab, lockmode);
 		}
 		else
 		{
@@ -16101,8 +16169,7 @@ RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab)
 			 * or if it is a clustered index, so that ATPostAlterTypeCleanup()
 			 * can queue up commands necessary to restore those properties.
 			 */
-			RememberReplicaIdentityForRebuilding(indoid, tab);
-			RememberClusterOnForRebuilding(indoid, tab);
+			RememberIndexPropertiesForRebuilding(indoid, tab, lockmode);
 		}
 	}
 }
@@ -16280,34 +16347,53 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 	/*
 	 * Queue up command to restore replica identity index marking
 	 */
-	if (tab->replicaIdentityIndex)
+	if (tab->replicaIdentityIndexOids != NIL)
 	{
-		AlterTableCmd *cmd = makeNode(AlterTableCmd);
-		ReplicaIdentityStmt *subcmd = makeNode(ReplicaIdentityStmt);
-
-		subcmd->identity_type = REPLICA_IDENTITY_INDEX;
-		subcmd->name = tab->replicaIdentityIndex;
-		cmd->subtype = AT_ReplicaIdentity;
-		cmd->def = (Node *) subcmd;
-
-		/* do it after indexes and constraints */
-		tab->subcmds[AT_PASS_OLD_CONSTR] =
-			lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd);
+		foreach_oid(indexId, tab->replicaIdentityIndexOids)
+		{
+			Oid			relId = IndexGetRelation(indexId, false);
+			Relation	partrel;
+			AlteredTableInfo *parttab;
+
+			AlterTableCmd *cmd = makeNode(AlterTableCmd);
+			ReplicaIdentityStmt *subcmd = makeNode(ReplicaIdentityStmt);
+
+			subcmd->identity_type = REPLICA_IDENTITY_INDEX;
+			subcmd->name = get_rel_name(indexId);
+			cmd->subtype = AT_ReplicaIdentity;
+			cmd->def = (Node *) subcmd;
+
+			/* do it after indexes and constraints */
+			partrel = relation_open(relId, lockmode);
+			parttab = ATGetQueueEntry(wqueue, partrel);
+			parttab->subcmds[AT_PASS_OLD_CONSTR] =
+				lappend(parttab->subcmds[AT_PASS_OLD_CONSTR], cmd);
+			relation_close(partrel, NoLock);
+		}
 	}
 
 	/*
 	 * Queue up command to restore marking of index used for cluster.
 	 */
-	if (tab->clusterOnIndex)
+	if (tab->clusterOnIndexOids != NIL)
 	{
-		AlterTableCmd *cmd = makeNode(AlterTableCmd);
-
-		cmd->subtype = AT_ClusterOn;
-		cmd->name = tab->clusterOnIndex;
-
-		/* do it after indexes and constraints */
-		tab->subcmds[AT_PASS_OLD_CONSTR] =
-			lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd);
+		foreach_oid(indexId, tab->clusterOnIndexOids)
+		{
+			Oid			relId = IndexGetRelation(indexId, false);
+			Relation	partrel;
+			AlteredTableInfo *parttab;
+			AlterTableCmd *cmd = makeNode(AlterTableCmd);
+
+			cmd->subtype = AT_ClusterOn;
+			cmd->name = get_rel_name(indexId);
+
+			/* do it after indexes and constraints */
+			partrel = relation_open(relId, lockmode);
+			parttab = ATGetQueueEntry(wqueue, partrel);
+			parttab->subcmds[AT_PASS_OLD_CONSTR] =
+				lappend(parttab->subcmds[AT_PASS_OLD_CONSTR], cmd);
+			relation_close(partrel, NoLock);
+		}
 	}
 
 	/*
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 44d08bc4d0e..094bcffdd4e 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -4836,20 +4836,25 @@ select indexrelid::regclass, indisclustered from pg_index
 (2 rows)
 
 drop table alttype_cluster;
--- A partition index keeps its name when rebuilt through the parent.
+-- Partition indexes keep their names and clustered status when rebuilt
+-- through the parent.
 create table alttype_part (a int not null, b int) partition by range (a);
 create table alttype_part_child (a int not null, b int);
 create index alttype_part_child_custom_idx on alttype_part_child (a, b);
 create index alttype_part_idx on alttype_part (a, b);
 alter table alttype_part attach partition alttype_part_child
   for values from (0) to (10);
+create index alttype_part_other_idx on alttype_part (b);
+alter table alttype_part_child cluster on alttype_part_child_custom_idx;
 alter table alttype_part alter column b type bigint;
-select indexrelid::regclass
-  from pg_index where indrelid = 'alttype_part_child'::regclass;
-          indexrelid           
--------------------------------
- alttype_part_child_custom_idx
-(1 row)
+select indexrelid::regclass, indisclustered
+  from pg_index where indrelid = 'alttype_part_child'::regclass
+  order by indexrelid::regclass::text;
+          indexrelid           | indisclustered 
+-------------------------------+----------------
+ alttype_part_child_b_idx      | f
+ alttype_part_child_custom_idx | t
+(2 rows)
 
 drop table alttype_part;
 --
diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out
index 336b04fa278..0d13e33fe32 100644
--- a/src/test/regress/expected/replica_identity.out
+++ b/src/test/regress/expected/replica_identity.out
@@ -292,6 +292,113 @@ ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey;
 ERROR:  constraint "test_replica_identity5_pkey" of relation "test_replica_identity5" does not exist
 ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL;
 ERROR:  column "b" is in index used as replica identity
+--
+-- Test index rebuild preserves replica identity against partitioned tables
+--
+CREATE TABLE test_replica_identity_partitioned (id int NOT NULL, val int NOT NULL) PARTITION BY RANGE (id);
+CREATE TABLE test_replica_identity_partitioned_p1 PARTITION OF test_replica_identity_partitioned
+    FOR VALUES FROM (0) TO (100);
+CREATE TABLE test_replica_identity_partitioned_p2 PARTITION OF test_replica_identity_partitioned
+    FOR VALUES FROM (101) TO (200) PARTITION BY LIST (id);
+CREATE TABLE test_replica_identity_partitioned_p2_1 PARTITION OF test_replica_identity_partitioned_p2
+    FOR VALUES IN (150, 160);
+-- For better coverage, create a partition in a different schema
+CREATE SCHEMA test_replica_identity_schema;
+CREATE TABLE test_replica_identity_schema.test_replica_identity_partitioned_p2_2 PARTITION OF test_replica_identity_partitioned_p2
+    FOR VALUES IN (170, 180);
+CREATE UNIQUE INDEX test_replica_identity_partitioned_pkey ON test_replica_identity_partitioned (id, val);
+ALTER TABLE test_replica_identity_partitioned REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_pkey;
+ALTER TABLE test_replica_identity_partitioned_p1 REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_p1_id_val_idx;
+ALTER TABLE test_replica_identity_partitioned_p2_1 REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_p2_1_id_val_idx;
+ALTER TABLE test_replica_identity_schema.test_replica_identity_partitioned_p2_2 REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_p2_2_id_val_idx;
+SELECT tc.relname as table_name, tc.relreplident, i.indexrelid::regclass AS index_name, i.indisreplident FROM pg_class c
+  LEFT JOIN pg_index i ON c.oid = i.indexrelid
+  LEFT JOIN pg_class tc ON i.indrelid = tc.oid
+  WHERE c.relname LIKE 'test_replica_identity_partitioned%' and c.relkind in ('i', 'I')
+  ORDER BY c.relname;
+               table_name               | relreplident |                                   index_name                                   | indisreplident 
+----------------------------------------+--------------+--------------------------------------------------------------------------------+----------------
+ test_replica_identity_partitioned_p1   | i            | test_replica_identity_partitioned_p1_id_val_idx                                | t
+ test_replica_identity_partitioned_p2_1 | i            | test_replica_identity_partitioned_p2_1_id_val_idx                              | t
+ test_replica_identity_partitioned_p2_2 | i            | test_replica_identity_schema.test_replica_identity_partitioned_p2_2_id_val_idx | t
+ test_replica_identity_partitioned_p2   | d            | test_replica_identity_partitioned_p2_id_val_idx                                | f
+ test_replica_identity_partitioned      | i            | test_replica_identity_partitioned_pkey                                         | t
+(5 rows)
+
+-- Store the index state before rebuild
+SELECT array_agg(c.oid ORDER BY c.relname) AS index_oids,
+       array_agg(c.relname::text ORDER BY c.relname) AS index_names,
+       array_agg(n.nspname ORDER BY c.relname) AS schema_names,
+       array_agg(i.indisreplident ORDER BY c.relname) AS indisreplidents
+  FROM pg_class c
+  JOIN pg_namespace n ON c.relnamespace = n.oid
+  JOIN pg_index i ON c.oid = i.indexrelid
+  WHERE c.relname LIKE 'test_replica_identity_partitioned%' AND c.relkind IN ('i', 'I') \gset before_
+ALTER TABLE test_replica_identity_partitioned ALTER COLUMN val TYPE bigint;
+SELECT tc.relname as table_name, tc.relreplident, i.indexrelid::regclass AS index_name, i.indisreplident FROM pg_class c
+  LEFT JOIN pg_index i ON c.oid = i.indexrelid
+  LEFT JOIN pg_class tc ON i.indrelid = tc.oid
+  WHERE c.relname LIKE 'test_replica_identity_partitioned%' and c.relkind in ('i', 'I')
+  ORDER BY c.relname;
+               table_name               | relreplident |                                   index_name                                   | indisreplident 
+----------------------------------------+--------------+--------------------------------------------------------------------------------+----------------
+ test_replica_identity_partitioned_p1   | i            | test_replica_identity_partitioned_p1_id_val_idx                                | t
+ test_replica_identity_partitioned_p2_1 | i            | test_replica_identity_partitioned_p2_1_id_val_idx                              | t
+ test_replica_identity_partitioned_p2_2 | i            | test_replica_identity_schema.test_replica_identity_partitioned_p2_2_id_val_idx | t
+ test_replica_identity_partitioned_p2   | d            | test_replica_identity_partitioned_p2_id_val_idx                                | f
+ test_replica_identity_partitioned      | i            | test_replica_identity_partitioned_pkey                                         | t
+(5 rows)
+
+-- Compare the before and after to ensure replica identity preserved
+SELECT b.index_name, b.index_oid != c.oid AS rebuilt,
+       b.indisreplident != i.indisreplident AS ri_lost
+  FROM unnest(:'before_index_oids'::oid[],
+              :'before_index_names'::text[],
+              :'before_schema_names'::text[],
+              :'before_indisreplidents'::bool[])
+       AS b(index_oid, index_name, schema_name, indisreplident)
+  JOIN pg_namespace n ON n.nspname = b.schema_name
+  JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = b.index_name
+  JOIN pg_index i ON c.oid = i.indexrelid
+  ORDER BY b.index_name;
+                    index_name                     | rebuilt | ri_lost 
+---------------------------------------------------+---------+---------
+ test_replica_identity_partitioned_p1_id_val_idx   | t       | f
+ test_replica_identity_partitioned_p2_1_id_val_idx | t       | f
+ test_replica_identity_partitioned_p2_2_id_val_idx | t       | f
+ test_replica_identity_partitioned_p2_id_val_idx   | t       | f
+ test_replica_identity_partitioned_pkey            | t       | f
+(5 rows)
+
+DROP SCHEMA test_replica_identity_schema CASCADE;
+NOTICE:  drop cascades to table test_replica_identity_schema.test_replica_identity_partitioned_p2_2
+DROP TABLE test_replica_identity_partitioned;
+-- A partition may use a partition index as replica identity even when its
+-- parent does not use the corresponding partitioned index.
+CREATE TABLE test_replica_identity_child_only (id int NOT NULL, val int NOT NULL) PARTITION BY RANGE (id);
+CREATE TABLE test_replica_identity_child_only_p1 (id int NOT NULL, val int NOT NULL);
+CREATE UNIQUE INDEX test_replica_identity_child_custom_idx
+    ON test_replica_identity_child_only_p1 (id, val);
+ALTER TABLE test_replica_identity_child_only_p1
+    REPLICA IDENTITY USING INDEX test_replica_identity_child_custom_idx;
+CREATE UNIQUE INDEX test_replica_identity_child_only_idx ON test_replica_identity_child_only (id, val);
+ALTER TABLE test_replica_identity_child_only ATTACH PARTITION test_replica_identity_child_only_p1
+    FOR VALUES FROM (0) TO (100);
+ALTER TABLE test_replica_identity_child_only ALTER COLUMN val TYPE bigint;
+SELECT tc.relname AS table_name, tc.relreplident, i.indexrelid::regclass AS index_name, i.indisreplident
+  FROM pg_class c
+  JOIN pg_index i ON c.oid = i.indexrelid
+  JOIN pg_class tc ON i.indrelid = tc.oid
+  WHERE tc.relname = 'test_replica_identity_child_only_p1' AND i.indisreplident;
+             table_name              | relreplident |               index_name               | indisreplident 
+-------------------------------------+--------------+----------------------------------------+----------------
+ test_replica_identity_child_only_p1 | i            | test_replica_identity_child_custom_idx | t
+(1 row)
+
+DROP TABLE test_replica_identity_child_only;
+--
+-- Cleanup
+--
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity2;
 DROP TABLE test_replica_identity3;
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 48f5f0b8bbc..74ee7f8a9bd 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -3128,16 +3128,20 @@ select indexrelid::regclass, indisclustered from pg_index
   order by indexrelid::regclass::text;
 drop table alttype_cluster;
 
--- A partition index keeps its name when rebuilt through the parent.
+-- Partition indexes keep their names and clustered status when rebuilt
+-- through the parent.
 create table alttype_part (a int not null, b int) partition by range (a);
 create table alttype_part_child (a int not null, b int);
 create index alttype_part_child_custom_idx on alttype_part_child (a, b);
 create index alttype_part_idx on alttype_part (a, b);
 alter table alttype_part attach partition alttype_part_child
   for values from (0) to (10);
+create index alttype_part_other_idx on alttype_part (b);
+alter table alttype_part_child cluster on alttype_part_child_custom_idx;
 alter table alttype_part alter column b type bigint;
-select indexrelid::regclass
-  from pg_index where indrelid = 'alttype_part_child'::regclass;
+select indexrelid::regclass, indisclustered
+  from pg_index where indrelid = 'alttype_part_child'::regclass
+  order by indexrelid::regclass::text;
 drop table alttype_part;
 
 --
diff --git a/src/test/regress/sql/replica_identity.sql b/src/test/regress/sql/replica_identity.sql
index 30daec05b71..082ba833df9 100644
--- a/src/test/regress/sql/replica_identity.sql
+++ b/src/test/regress/sql/replica_identity.sql
@@ -134,6 +134,82 @@ ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL;
 ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey;
 ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL;
 
+--
+-- Test index rebuild preserves replica identity against partitioned tables
+--
+CREATE TABLE test_replica_identity_partitioned (id int NOT NULL, val int NOT NULL) PARTITION BY RANGE (id);
+CREATE TABLE test_replica_identity_partitioned_p1 PARTITION OF test_replica_identity_partitioned
+    FOR VALUES FROM (0) TO (100);
+CREATE TABLE test_replica_identity_partitioned_p2 PARTITION OF test_replica_identity_partitioned
+    FOR VALUES FROM (101) TO (200) PARTITION BY LIST (id);
+CREATE TABLE test_replica_identity_partitioned_p2_1 PARTITION OF test_replica_identity_partitioned_p2
+    FOR VALUES IN (150, 160);
+-- For better coverage, create a partition in a different schema
+CREATE SCHEMA test_replica_identity_schema;
+CREATE TABLE test_replica_identity_schema.test_replica_identity_partitioned_p2_2 PARTITION OF test_replica_identity_partitioned_p2
+    FOR VALUES IN (170, 180);
+CREATE UNIQUE INDEX test_replica_identity_partitioned_pkey ON test_replica_identity_partitioned (id, val);
+ALTER TABLE test_replica_identity_partitioned REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_pkey;
+ALTER TABLE test_replica_identity_partitioned_p1 REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_p1_id_val_idx;
+ALTER TABLE test_replica_identity_partitioned_p2_1 REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_p2_1_id_val_idx;
+ALTER TABLE test_replica_identity_schema.test_replica_identity_partitioned_p2_2 REPLICA IDENTITY USING INDEX test_replica_identity_partitioned_p2_2_id_val_idx;
+SELECT tc.relname as table_name, tc.relreplident, i.indexrelid::regclass AS index_name, i.indisreplident FROM pg_class c
+  LEFT JOIN pg_index i ON c.oid = i.indexrelid
+  LEFT JOIN pg_class tc ON i.indrelid = tc.oid
+  WHERE c.relname LIKE 'test_replica_identity_partitioned%' and c.relkind in ('i', 'I')
+  ORDER BY c.relname;
+-- Store the index state before rebuild
+SELECT array_agg(c.oid ORDER BY c.relname) AS index_oids,
+       array_agg(c.relname::text ORDER BY c.relname) AS index_names,
+       array_agg(n.nspname ORDER BY c.relname) AS schema_names,
+       array_agg(i.indisreplident ORDER BY c.relname) AS indisreplidents
+  FROM pg_class c
+  JOIN pg_namespace n ON c.relnamespace = n.oid
+  JOIN pg_index i ON c.oid = i.indexrelid
+  WHERE c.relname LIKE 'test_replica_identity_partitioned%' AND c.relkind IN ('i', 'I') \gset before_
+ALTER TABLE test_replica_identity_partitioned ALTER COLUMN val TYPE bigint;
+SELECT tc.relname as table_name, tc.relreplident, i.indexrelid::regclass AS index_name, i.indisreplident FROM pg_class c
+  LEFT JOIN pg_index i ON c.oid = i.indexrelid
+  LEFT JOIN pg_class tc ON i.indrelid = tc.oid
+  WHERE c.relname LIKE 'test_replica_identity_partitioned%' and c.relkind in ('i', 'I')
+  ORDER BY c.relname;
+-- Compare the before and after to ensure replica identity preserved
+SELECT b.index_name, b.index_oid != c.oid AS rebuilt,
+       b.indisreplident != i.indisreplident AS ri_lost
+  FROM unnest(:'before_index_oids'::oid[],
+              :'before_index_names'::text[],
+              :'before_schema_names'::text[],
+              :'before_indisreplidents'::bool[])
+       AS b(index_oid, index_name, schema_name, indisreplident)
+  JOIN pg_namespace n ON n.nspname = b.schema_name
+  JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = b.index_name
+  JOIN pg_index i ON c.oid = i.indexrelid
+  ORDER BY b.index_name;
+DROP SCHEMA test_replica_identity_schema CASCADE;
+DROP TABLE test_replica_identity_partitioned;
+
+-- A partition may use a partition index as replica identity even when its
+-- parent does not use the corresponding partitioned index.
+CREATE TABLE test_replica_identity_child_only (id int NOT NULL, val int NOT NULL) PARTITION BY RANGE (id);
+CREATE TABLE test_replica_identity_child_only_p1 (id int NOT NULL, val int NOT NULL);
+CREATE UNIQUE INDEX test_replica_identity_child_custom_idx
+    ON test_replica_identity_child_only_p1 (id, val);
+ALTER TABLE test_replica_identity_child_only_p1
+    REPLICA IDENTITY USING INDEX test_replica_identity_child_custom_idx;
+CREATE UNIQUE INDEX test_replica_identity_child_only_idx ON test_replica_identity_child_only (id, val);
+ALTER TABLE test_replica_identity_child_only ATTACH PARTITION test_replica_identity_child_only_p1
+    FOR VALUES FROM (0) TO (100);
+ALTER TABLE test_replica_identity_child_only ALTER COLUMN val TYPE bigint;
+SELECT tc.relname AS table_name, tc.relreplident, i.indexrelid::regclass AS index_name, i.indisreplident
+  FROM pg_class c
+  JOIN pg_index i ON c.oid = i.indexrelid
+  JOIN pg_class tc ON i.indrelid = tc.oid
+  WHERE tc.relname = 'test_replica_identity_child_only_p1' AND i.indisreplident;
+DROP TABLE test_replica_identity_child_only;
+
+--
+-- Cleanup
+--
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity2;
 DROP TABLE test_replica_identity3;
-- 
2.50.1 (Apple Git-155)

