From 37778708a48087f70496fcd649c4a59d873e0644 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v7] Preserve extension dependencies on indexes during
 partition merge/split

When using ALTER TABLE ... MERGE PARTITIONS or ALTER TABLE ... SPLIT
PARTITION, extension dependencies on partition indexes were being lost.
This happened because the new partition indexes are created fresh from
the parent partitioned table's indexes, while the old partition indexes
(with their extension dependencies) are dropped.

Fix this by collecting extension dependencies from source partition
indexes before detaching them, then applying those dependencies to the
corresponding new partition indexes after they're created.  The mapping
between old and new indexes is done via their common parent partitioned
index.

For MERGE operations, all source partition indexes sharing a parent
partitioned index must have the same extension dependencies; if they
differ, an error naming both conflicting partition indexes is raised.
The check is implemented by collecting one entry per partition index,
sorting by parent index OID, and comparing adjacent entries in a single
pass.  This is order-independent: the same set of partitions produces
the same decision regardless of the order they are listed in the MERGE
command, and subset mismatches are caught in both directions.

For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.

The regression tests exercising this feature live under
src/test/modules/test_extensions, where the test_ext3 and test_ext5
extensions are available; core regression tests cannot assume any
particular extension is installed.

Author: Matheus Alcantara <matheusssilv97@gmail.com>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <d.koval@postgrespro.ru>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gmail.com
---
 doc/src/sgml/ref/alter_table.sgml             |  17 ++
 src/backend/commands/tablecmds.c              | 283 ++++++++++++++++++
 .../expected/test_extdepend.out               | 120 ++++++++
 .../test_extensions/sql/test_extdepend.sql    | 104 +++++++
 src/tools/pgindent/typedefs.list              |   1 +
 5 files changed, 525 insertions(+)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 54f38cb964a..1f9a456fd33 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1255,6 +1255,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       (see <xref linkend="ddl-depend"/>).
      </para>
 
+     <para>
+      Extension dependencies on partition indexes (created via
+      <link linkend="sql-alterindex"><command>ALTER INDEX ... DEPENDS ON
+      EXTENSION</command></link>) are preserved during merge operations.
+      All source partition indexes must have the same extension dependencies;
+      if they differ, an error is raised. This ensures that extension
+      dependencies are not silently lost during merge.
+     </para>
+
      <note>
       <para>
        Merging partitions acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
@@ -1342,6 +1351,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       would fail (see <xref linkend="ddl-depend"/>).
      </para>
 
+     <para>
+      Extension dependencies on partition indexes (created via
+      <link linkend="sql-alterindex"><command>ALTER INDEX ... DEPENDS ON
+      EXTENSION</command></link>) are preserved during split operations.
+      The new partitions' indexes will inherit the extension dependencies
+      from the source partition's indexes.
+     </para>
+
      <note>
       <para>
        Split partition acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index eec09ba1ded..38532341a92 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
+#include "catalog/pg_extension_d.h"
 #include "catalog/pg_foreign_table.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_largeobject.h"
@@ -60,6 +61,7 @@
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
+#include "commands/extension.h"
 #include "commands/repack.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
@@ -365,6 +367,27 @@ typedef enum addFkConstraintSides
 	addFkBothSides,
 } addFkConstraintSides;
 
+/*
+ * Hold extension dependencies of one partition index, during
+ * MERGE/SPLIT PARTITION processing.
+ *
+ * collectPartitionIndexExtDeps() builds a list of these entries sorted by
+ * parentIndexOid with exactly one entry per parent partitioned index; the
+ * list is then consumed by applyPartitionIndexExtDeps() to re-record the
+ * same dependencies on the newly created partition's indexes.
+ *
+ * extensionOids is kept sorted ascending so that equality checks between
+ * entries from different partitions can be done in a single pass.
+ * indexOid is carried only so that conflict errors can cite specific
+ * partition index names.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+	Oid			parentIndexOid; /* OID of the parent partitioned index */
+	Oid			indexOid;		/* OID of a representative partition index */
+	List	   *extensionOids;	/* OIDs of dependent extensions, sorted asc */
+} PartitionIndexExtDepEntry;
+
 /*
  * Partition tables are expected to be dropped when the parent partitioned
  * table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +783,9 @@ static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation
 static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
 								 Relation rel, PartitionCmd *cmd,
 								 AlterTableUtilityContext *context);
+static List *collectPartitionIndexExtDeps(List *partitionOids);
+static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState);
+static void freePartitionIndexExtDeps(List *extDepState);
 
 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -23000,6 +23026,224 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
 	PopActiveSnapshot();
 }
 
+/*
+ * equal_oid_lists: return true if two OID lists, each sorted in ascending
+ * order, contain the same OIDs in the same order.
+ */
+static bool
+equal_oid_lists(const List *a, const List *b)
+{
+	ListCell   *la,
+			   *lb;
+
+	if (list_length(a) != list_length(b))
+		return false;
+
+	forboth(la, a, lb, b)
+	{
+		if (lfirst_oid(la) != lfirst_oid(lb))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Comparator for list_sort() on a list of PartitionIndexExtDepEntry *.
+ * Orders by parentIndexOid, then by indexOid as a tiebreaker so conflict
+ * reports for different parent indexes are deterministic.
+ */
+static int
+cmp_partition_index_ext_dep(const ListCell *a, const ListCell *b)
+{
+	const PartitionIndexExtDepEntry *ea = lfirst(a);
+	const PartitionIndexExtDepEntry *eb = lfirst(b);
+
+	if (ea->parentIndexOid != eb->parentIndexOid)
+		return pg_cmp_u32(ea->parentIndexOid, eb->parentIndexOid);
+	return pg_cmp_u32(ea->indexOid, eb->indexOid);
+}
+
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. All source partition indexes sharing the same
+ * parent partitioned index must depend on exactly the same set of
+ * extensions; otherwise an error is raised so that we neither silently drop
+ * nor silently add dependencies on the merged partition's index.
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * The returned list is sorted by parentIndexOid with exactly one entry per
+ * parent partitioned index, so applyPartitionIndexExtDeps() can scan it
+ * linearly.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+	List	   *collected = NIL;
+	List	   *result = NIL;
+	PartitionIndexExtDepEntry *prev = NULL;
+
+	/*
+	 * Phase 1: collect one entry per (partition index -> parent index) pair,
+	 * with its extension dependency OIDs sorted ascending.
+	 */
+	foreach_oid(partOid, partitionOids)
+	{
+		Relation	partRel;
+		List	   *indexList;
+
+		/*
+		 * Use NoLock since the caller already holds AccessExclusiveLock on
+		 * these partitions.
+		 */
+		partRel = table_open(partOid, NoLock);
+		indexList = RelationGetIndexList(partRel);
+
+		foreach_oid(indexOid, indexList)
+		{
+			Oid			parentIndexOid;
+			PartitionIndexExtDepEntry *entry;
+
+			if (!get_rel_relispartition(indexOid))
+				continue;
+
+			parentIndexOid = get_partition_parent(indexOid, true);
+			if (!OidIsValid(parentIndexOid))
+				continue;
+
+			entry = palloc(sizeof(PartitionIndexExtDepEntry));
+			entry->parentIndexOid = parentIndexOid;
+			entry->indexOid = indexOid;
+			entry->extensionOids = getAutoExtensionsOfObject(RelationRelationId,
+															 indexOid);
+			list_sort(entry->extensionOids, list_oid_cmp);
+
+			collected = lappend(collected, entry);
+		}
+
+		list_free(indexList);
+		table_close(partRel, NoLock);
+	}
+
+	/*
+	 * Phase 2: sort by parentIndexOid so entries sharing a parent index sit
+	 * adjacent.
+	 */
+	list_sort(collected, cmp_partition_index_ext_dep);
+
+	/*
+	 * Phase 3: single linear pass verifying that adjacent entries sharing a
+	 * parent index have identical extension dependencies, and keeping one
+	 * representative entry per parent index.
+	 */
+	foreach_ptr(PartitionIndexExtDepEntry, entry, collected)
+	{
+		if (prev != NULL && prev->parentIndexOid == entry->parentIndexOid)
+		{
+			if (!equal_oid_lists(prev->extensionOids, entry->extensionOids))
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot merge partitions with conflicting extension dependencies"),
+						 errdetail("Partition indexes \"%s\" and \"%s\" depend on different extensions.",
+								   get_rel_name(prev->indexOid),
+								   get_rel_name(entry->indexOid))));
+
+			/* Duplicate entry for the same parent index; discard. */
+			list_free(entry->extensionOids);
+			pfree(entry);
+			continue;
+		}
+
+		result = lappend(result, entry);
+		prev = entry;
+	}
+
+	list_free(collected);
+
+	return result;
+}
+
+/*
+ * applyPartitionIndexExtDeps: apply collected extension dependencies to
+ * indexes on a new partition.
+ *
+ * For each index on the new partition, look up its parent index in the
+ * extDepState list. If found, record extension dependencies on the new index.
+ * extDepState is sorted by parentIndexOid, so the inner scan can bail out
+ * as soon as it passes the target OID.
+ */
+static void
+applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState)
+{
+	Relation	partRel;
+	List	   *indexList;
+
+	if (extDepState == NIL)
+		return;
+
+	/*
+	 * Use NoLock since the caller already holds AccessExclusiveLock on the
+	 * new partition.
+	 */
+	partRel = table_open(newPartOid, NoLock);
+	indexList = RelationGetIndexList(partRel);
+
+	foreach_oid(indexOid, indexList)
+	{
+		Oid			parentIdxOid;
+
+		if (!get_rel_relispartition(indexOid))
+			continue;
+
+		parentIdxOid = get_partition_parent(indexOid, true);
+		if (!OidIsValid(parentIdxOid))
+			continue;
+
+		foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState)
+		{
+			ObjectAddress indexAddr;
+
+			if (entry->parentIndexOid > parentIdxOid)
+				break;
+			if (entry->parentIndexOid < parentIdxOid)
+				continue;
+
+			ObjectAddressSet(indexAddr, RelationRelationId, indexOid);
+
+			foreach_oid(extOid, entry->extensionOids)
+			{
+				ObjectAddress extAddr;
+
+				ObjectAddressSet(extAddr, ExtensionRelationId, extOid);
+				recordDependencyOn(&indexAddr, &extAddr,
+								   DEPENDENCY_AUTO_EXTENSION);
+			}
+			break;
+		}
+	}
+
+	list_free(indexList);
+	table_close(partRel, NoLock);
+}
+
+/*
+ * freePartitionIndexExtDeps: free memory allocated by collectPartitionIndexExtDeps.
+ */
+static void
+freePartitionIndexExtDeps(List *extDepState)
+{
+	foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState)
+	{
+		list_free(entry->extensionOids);
+		pfree(entry);
+	}
+	list_free(extDepState);
+}
+
 /*
  * ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
  */
@@ -23009,6 +23253,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 {
 	Relation	newPartRel;
 	List	   *mergingPartitions = NIL;
+	List	   *extDepState = NIL;
 	Oid			defaultPartOid;
 	Oid			existingRelid;
 	Oid			ownerId = InvalidOid;
@@ -23098,6 +23343,13 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	defaultPartOid =
 		get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
 
+	/*
+	 * Collect extension dependencies from indexes on the merging partitions.
+	 * We must do this before detaching them, so we can restore the
+	 * dependencies on the new partition's indexes later.
+	 */
+	extDepState = collectPartitionIndexExtDeps(mergingPartitions);
+
 	/* Detach all merging partitions. */
 	foreach_oid(mergingPartitionOid, mergingPartitions)
 	{
@@ -23175,6 +23427,15 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
 
+	/*
+	 * Apply extension dependencies to the new partition's indexes. This
+	 * preserves any "DEPENDS ON EXTENSION" settings from the merged
+	 * partitions.
+	 */
+	applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState);
+
+	freePartitionIndexExtDeps(extDepState);
+
 	/* Keep the lock until commit. */
 	table_close(newPartRel, NoLock);
 
@@ -23469,11 +23730,13 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		isSameName = false;
 	char		tmpRelName[NAMEDATALEN];
 	List	   *newPartRels = NIL;
+	List	   *extDepState = NIL;
 	ObjectAddress object;
 	Oid			defaultPartOid;
 	Oid			save_userid;
 	int			save_sec_context;
 	int			save_nestlevel;
+	List	   *splitPartList;
 
 	defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
 
@@ -23506,6 +23769,16 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 					errmsg("relation \"%s\" already exists", sps->name->relname));
 	}
 
+	/*
+	 * Collect extension dependencies from indexes on the split partition. We
+	 * must do this before detaching it, so we can restore the dependencies on
+	 * the new partitions' indexes later.
+	 */
+	splitPartList = list_make1_oid(splitRelOid);
+
+	extDepState = collectPartitionIndexExtDeps(splitPartList);
+	list_free(splitPartList);
+
 	/* Detach the split partition. */
 	detachPartitionTable(rel, splitRel, defaultPartOid);
 
@@ -23585,10 +23858,20 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * needed.
 		 */
 		attachPartitionTable(NULL, rel, newPartRel, sps->bound);
+
+		/*
+		 * Apply extension dependencies to the new partition's indexes. This
+		 * preserves any "DEPENDS ON EXTENSION" settings from the split
+		 * partition.
+		 */
+		applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState);
+
 		/* Keep the lock until commit. */
 		table_close(newPartRel, NoLock);
 	}
 
+	freePartitionIndexExtDeps(extDepState);
+
 	/* Drop the split partition. */
 	object.classId = RelationRelationId;
 	object.objectId = splitRelOid;
diff --git a/src/test/modules/test_extensions/expected/test_extdepend.out b/src/test/modules/test_extensions/expected/test_extdepend.out
index 0b62015d18c..ede5dc64c04 100644
--- a/src/test/modules/test_extensions/expected/test_extdepend.out
+++ b/src/test/modules/test_extensions/expected/test_extdepend.out
@@ -186,3 +186,123 @@ DROP MATERIALIZED VIEW d;
 DROP INDEX e;
 DROP SCHEMA test_ext CASCADE;
 NOTICE:  drop cascades to table a
+-- Fifth test: extension dependencies on partition indexes survive MERGE and
+-- SPLIT PARTITION operations, and mismatches between source partitions are
+-- reported.
+RESET search_path;
+CREATE EXTENSION test_ext3;
+CREATE EXTENSION test_ext5;
+CREATE TABLE part_extdep (i int, x int) PARTITION BY RANGE (i);
+CREATE TABLE part_extdep_1 PARTITION OF part_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE part_extdep_2 PARTITION OF part_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE part_extdep_3 PARTITION OF part_extdep FOR VALUES FROM (3) TO (4);
+CREATE TABLE part_extdep_4 PARTITION OF part_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE part_extdep_5 PARTITION OF part_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX part_extdep_i_idx ON part_extdep(i);
+CREATE INDEX part_extdep_x_idx ON part_extdep(x);
+-- Partitions 1, 2, 3 depend on the same two extensions.
+ALTER INDEX part_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX part_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX part_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Partition 4 depends on a different extension on one index.
+ALTER INDEX part_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+-- Partition 5 has no dependency at all.
+-- Merge matching partitions: should succeed and preserve dependencies on the
+-- new partition's indexes (DROP EXTENSION must fail, naming the new index).
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_1, part_extdep_2)
+    INTO part_extdep_merged;
+DROP EXTENSION test_ext3;
+ERROR:  cannot drop index part_extdep_merged_i_idx because index part_extdep_i_idx requires it
+HINT:  You can drop index part_extdep_i_idx instead.
+SELECT c.relname, e.extname
+FROM pg_depend d
+JOIN pg_class c ON d.objid = c.oid
+JOIN pg_extension e ON d.refobjid = e.oid
+WHERE c.relname IN ('part_extdep_merged_i_idx', 'part_extdep_merged_x_idx')
+  AND e.extname IN ('test_ext3', 'test_ext5')
+  AND d.deptype = 'x'
+ORDER BY c.relname, e.extname;
+         relname          |  extname  
+--------------------------+-----------
+ part_extdep_merged_i_idx | test_ext3
+ part_extdep_merged_x_idx | test_ext5
+(2 rows)
+
+-- An index created directly on a partition has no parent in the partitioned
+-- index tree.  Such an index is dropped with its old partition during merge,
+-- and any extension dependency it carries goes away with it: the dep is not
+-- promoted to the merged partition.  Verify by attaching test_ext9 to such
+-- an orphan index, merging, and observing that test_ext9 becomes droppable.
+CREATE EXTENSION test_ext9;
+CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
+ALTER INDEX part_extdep_3_extra_idx DEPENDS ON EXTENSION test_ext9;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
+    INTO part_extdep_merged2;
+DROP EXTENSION test_ext9;
+-- Mismatched dependencies: partition 4's index depends on a different
+-- extension than partition_merged2's. Both orderings must fail, and the
+-- error must cite both partition indexes.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged2, part_extdep_4)
+    INTO part_extdep_bad;
+ERROR:  cannot merge partitions with conflicting extension dependencies
+DETAIL:  Partition indexes "part_extdep_4_i_idx" and "part_extdep_merged2_i_idx" depend on different extensions.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_merged2)
+    INTO part_extdep_bad;
+ERROR:  cannot merge partitions with conflicting extension dependencies
+DETAIL:  Partition indexes "part_extdep_4_i_idx" and "part_extdep_merged2_i_idx" depend on different extensions.
+-- Empty vs non-empty dependency set (the subset case the earlier linear
+-- check missed in one direction).
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5)
+    INTO part_extdep_bad;
+ERROR:  cannot merge partitions with conflicting extension dependencies
+DETAIL:  Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4)
+    INTO part_extdep_bad;
+ERROR:  cannot merge partitions with conflicting extension dependencies
+DETAIL:  Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions.
+-- Subset: partition 5's i_idx depends on a strict superset of partition 4's
+-- i_idx dependencies. Partition 4 = {test_ext5}, partition 5 will be
+-- {test_ext3, test_ext5}. Both orderings must fail; in particular the case
+-- where the first partition we walk has fewer extensions than the second
+-- must still be rejected.
+ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext5;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5)
+    INTO part_extdep_bad;
+ERROR:  cannot merge partitions with conflicting extension dependencies
+DETAIL:  Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4)
+    INTO part_extdep_bad;
+ERROR:  cannot merge partitions with conflicting extension dependencies
+DETAIL:  Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions.
+-- Reset partition 5 so it doesn't interfere with the SPLIT test below.
+ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext5;
+-- Split: the single source partition's dependencies must appear on every
+-- new partition's matching index, identified by extension name.
+ALTER TABLE part_extdep SPLIT PARTITION part_extdep_merged2 INTO
+    (PARTITION part_extdep_s1 FOR VALUES FROM (1) TO (3),
+     PARTITION part_extdep_s2 FOR VALUES FROM (3) TO (4));
+SELECT c.relname, e.extname
+FROM pg_depend d
+JOIN pg_class c ON d.objid = c.oid
+JOIN pg_extension e ON d.refobjid = e.oid
+WHERE c.relname IN ('part_extdep_s1_i_idx', 'part_extdep_s1_x_idx',
+                    'part_extdep_s2_i_idx', 'part_extdep_s2_x_idx')
+  AND e.extname IN ('test_ext3', 'test_ext5')
+  AND d.deptype = 'x'
+ORDER BY c.relname, e.extname;
+       relname        |  extname  
+----------------------+-----------
+ part_extdep_s1_i_idx | test_ext3
+ part_extdep_s1_x_idx | test_ext5
+ part_extdep_s2_i_idx | test_ext3
+ part_extdep_s2_x_idx | test_ext5
+(4 rows)
+
+DROP TABLE part_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
diff --git a/src/test/modules/test_extensions/sql/test_extdepend.sql b/src/test/modules/test_extensions/sql/test_extdepend.sql
index 63240a1af5d..ad734af1e71 100644
--- a/src/test/modules/test_extensions/sql/test_extdepend.sql
+++ b/src/test/modules/test_extensions/sql/test_extdepend.sql
@@ -88,3 +88,107 @@ DROP FUNCTION b();
 DROP MATERIALIZED VIEW d;
 DROP INDEX e;
 DROP SCHEMA test_ext CASCADE;
+
+-- Fifth test: extension dependencies on partition indexes survive MERGE and
+-- SPLIT PARTITION operations, and mismatches between source partitions are
+-- reported.
+RESET search_path;
+CREATE EXTENSION test_ext3;
+CREATE EXTENSION test_ext5;
+
+CREATE TABLE part_extdep (i int, x int) PARTITION BY RANGE (i);
+CREATE TABLE part_extdep_1 PARTITION OF part_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE part_extdep_2 PARTITION OF part_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE part_extdep_3 PARTITION OF part_extdep FOR VALUES FROM (3) TO (4);
+CREATE TABLE part_extdep_4 PARTITION OF part_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE part_extdep_5 PARTITION OF part_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX part_extdep_i_idx ON part_extdep(i);
+CREATE INDEX part_extdep_x_idx ON part_extdep(x);
+
+-- Partitions 1, 2, 3 depend on the same two extensions.
+ALTER INDEX part_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX part_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX part_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Partition 4 depends on a different extension on one index.
+ALTER INDEX part_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Partition 5 has no dependency at all.
+
+-- Merge matching partitions: should succeed and preserve dependencies on the
+-- new partition's indexes (DROP EXTENSION must fail, naming the new index).
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_1, part_extdep_2)
+    INTO part_extdep_merged;
+DROP EXTENSION test_ext3;
+SELECT c.relname, e.extname
+FROM pg_depend d
+JOIN pg_class c ON d.objid = c.oid
+JOIN pg_extension e ON d.refobjid = e.oid
+WHERE c.relname IN ('part_extdep_merged_i_idx', 'part_extdep_merged_x_idx')
+  AND e.extname IN ('test_ext3', 'test_ext5')
+  AND d.deptype = 'x'
+ORDER BY c.relname, e.extname;
+
+-- An index created directly on a partition has no parent in the partitioned
+-- index tree.  Such an index is dropped with its old partition during merge,
+-- and any extension dependency it carries goes away with it: the dep is not
+-- promoted to the merged partition.  Verify by attaching test_ext9 to such
+-- an orphan index, merging, and observing that test_ext9 becomes droppable.
+CREATE EXTENSION test_ext9;
+CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
+ALTER INDEX part_extdep_3_extra_idx DEPENDS ON EXTENSION test_ext9;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
+    INTO part_extdep_merged2;
+DROP EXTENSION test_ext9;
+
+-- Mismatched dependencies: partition 4's index depends on a different
+-- extension than partition_merged2's. Both orderings must fail, and the
+-- error must cite both partition indexes.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged2, part_extdep_4)
+    INTO part_extdep_bad;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_merged2)
+    INTO part_extdep_bad;
+
+-- Empty vs non-empty dependency set (the subset case the earlier linear
+-- check missed in one direction).
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5)
+    INTO part_extdep_bad;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4)
+    INTO part_extdep_bad;
+
+-- Subset: partition 5's i_idx depends on a strict superset of partition 4's
+-- i_idx dependencies. Partition 4 = {test_ext5}, partition 5 will be
+-- {test_ext3, test_ext5}. Both orderings must fail; in particular the case
+-- where the first partition we walk has fewer extensions than the second
+-- must still be rejected.
+ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext5;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5)
+    INTO part_extdep_bad;
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4)
+    INTO part_extdep_bad;
+-- Reset partition 5 so it doesn't interfere with the SPLIT test below.
+ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext5;
+
+-- Split: the single source partition's dependencies must appear on every
+-- new partition's matching index, identified by extension name.
+ALTER TABLE part_extdep SPLIT PARTITION part_extdep_merged2 INTO
+    (PARTITION part_extdep_s1 FOR VALUES FROM (1) TO (3),
+     PARTITION part_extdep_s2 FOR VALUES FROM (3) TO (4));
+SELECT c.relname, e.extname
+FROM pg_depend d
+JOIN pg_class c ON d.objid = c.oid
+JOIN pg_extension e ON d.refobjid = e.oid
+WHERE c.relname IN ('part_extdep_s1_i_idx', 'part_extdep_s1_x_idx',
+                    'part_extdep_s2_i_idx', 'part_extdep_s2_x_idx')
+  AND e.extname IN ('test_ext3', 'test_ext5')
+  AND d.deptype = 'x'
+ORDER BY c.relname, e.extname;
+
+DROP TABLE part_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 49dfb662abc..9f1dd55213d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2200,6 +2200,7 @@ PartitionDirectoryEntry
 PartitionDispatch
 PartitionElem
 PartitionHashBound
+PartitionIndexExtDepEntry
 PartitionKey
 PartitionListValue
 PartitionMap
-- 
2.39.5 (Apple Git-154)

