public inbox for [email protected]
help / color / mirror / Atom feedMERGE PARTITIONS and DEPENDS ON EXTENSION.
21+ messages / 4 participants
[nested] [flat]
* MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-03-10 17:53 Kirill Reshke <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Kirill Reshke @ 2026-03-10 17:53 UTC (permalink / raw)
To: pgsql-hackers
Today, while reviewing another patch, I spotted PostgreSQL behaviour
which I cannot tell if is correct.
-- create relation
reshke=# create table pt (i int) partition by range ( i);
CREATE TABLE
-- create partitions.
reshke=# create table pt1 partition of pt for values from ( 1 ) to (2) ;
CREATE TABLE
reshke=# create table pt2 partition of pt for values from ( 2 ) to (3) ;
CREATE TABLE
-- manually add dependency on extension.
reshke=# alter index pt1_i_idx depends on extension btree_gist ;
ALTER INDEX
reshke=# alter index pt2_i_idx depends on extension btree_gist ;
ALTER INDEX
At this point, `drop extension btree_gist` fails due to existing
dependencies. However, after `alter table pt merge partitions ( pt1 ,
pt2 ) into pt3;` there are no dependencies, and drop extension
executes successfully.
My first impression was that there is no issue as the user created a
new database object, so should manually add dependency on extension.
However I am not 100% in this reasoning.
Any thoughts?
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-07 14:14 Matheus Alcantara <[email protected]>
parent: Kirill Reshke <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-07 14:14 UTC (permalink / raw)
To: Kirill Reshke <[email protected]>; pgsql-hackers
On Wed Mar 11, 2026 at 12:12 PM -03, Matheus Alcantara wrote:
> On 10/03/26 14:53, Kirill Reshke wrote:
>> Today, while reviewing another patch, I spotted PostgreSQL behaviour
>> which I cannot tell if is correct.
>>
>> -- create relation
>> reshke=# create table pt (i int) partition by range ( i);
>> CREATE TABLE
>>
>> -- create partitions.
>> reshke=# create table pt1 partition of pt for values from ( 1 ) to (2) ;
>> CREATE TABLE
>> reshke=# create table pt2 partition of pt for values from ( 2 ) to (3) ;
>> CREATE TABLE
>>
>> -- manually add dependency on extension.
>> reshke=# alter index pt1_i_idx depends on extension btree_gist ;
>> ALTER INDEX
>> reshke=# alter index pt2_i_idx depends on extension btree_gist ;
>> ALTER INDEX
>>
>> At this point, `drop extension btree_gist` fails due to existing
>> dependencies. However, after `alter table pt merge partitions ( pt1 ,
>> pt2 ) into pt3;` there are no dependencies, and drop extension
>> executes successfully.
>>
>> My first impression was that there is no issue as the user created a
>> new database object, so should manually add dependency on extension.
>> However I am not 100% in this reasoning.
>>
>> Any thoughts?
>>
>
> I'm also not sure if it's correct to assume that the dependency should
> be manually added after a partition is merged or splited but I was
> checking ATExecMergePartitions() and ATExecSplitPartition() and I
> think that it's not complicated to implement this.
>
> IIUC we just need to collect the extension dependencies before an
> index is detached on MERGE and SPLIT operations and then apply the
> dependency after the index is created on the new merged/splited
> partition. The attached patch implement this.
>
> Note that I'm using two different extensions for partition_merge and
> partition_split tests because I was having deadlock issues when
> running these tests in parallel using the same extension as a dependency.
>
Attaching a new rebased version, no changes compared with v1.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From 44c37e30e60b53d4478dbf32d4aae2a7d6b9b17f Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v2] 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 (created via
ALTER INDEX ... DEPENDS ON EXTENSION) 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.
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gma...
Reported-by: Kirill Reshke
---
src/backend/commands/tablecmds.c | 218 ++++++++++++++++++
src/test/regress/expected/partition_merge.out | 40 ++++
src/test/regress/expected/partition_split.out | 41 ++++
src/test/regress/sql/partition_merge.sql | 38 +++
src/test/regress/sql/partition_split.sql | 39 ++++
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 377 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index eec09ba1ded..afa89f8b3ce 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"
@@ -365,6 +366,16 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +771,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 +23014,171 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each index on the source partitions that has extension dependencies, we
+ * collect a mapping from the parent partitioned index OID to the list of
+ * extension OIDs.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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 parentIdxOid;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+ ListCell *lc;
+
+ /* Get the parent index if this is a partition index */
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Get extension dependencies for this index */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+ if (extDeps == NIL)
+ continue;
+
+ /*
+ * Look for existing entry for this parent index.
+ */
+ foreach(lc, result)
+ {
+ PartitionIndexExtDepEntry *e = lfirst(lc);
+
+ if (e->parentIndexOid == parentIdxOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (entry != NULL)
+ {
+ /* Add extension dependencies avoiding duplicates */
+ foreach_oid(extOid, extDeps)
+ {
+ if (!list_member_oid(entry->extensionOids, extOid))
+ entry->extensionOids = lappend_oid(entry->extensionOids,
+ extOid);
+ }
+ list_free(extDeps);
+ }
+ else
+ {
+ /* Create new entry */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIdxOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Get the parent index if this is a partition index */
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23188,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 +23278,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 +23362,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 +23665,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 +23704,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 +23793,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/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 925fe4f570a..00aa16964ce 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,46 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION if not exists btree_gist;
+CREATE TABLE t_merge_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+-- Add extension dependency on partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Verify the dependency exists in pg_depend
+SELECT COUNT(*) > 0 AS has_ext_dep
+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 = 't_merge_extdep_merged_i_idx'
+ AND e.extname = 'btree_gist'
+ AND d.deptype = 'x';
+ has_ext_dep
+-------------
+ t
+(1 row)
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 13ca733f9fa..a89c2dadc97 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..4864c66636d 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,44 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION if not exists btree_gist;
+
+CREATE TABLE t_merge_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+
+-- Add extension dependency on partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+
+-- Verify the dependency exists in pg_depend
+SELECT COUNT(*) > 0 AS has_ext_dep
+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 = 't_merge_extdep_merged_i_idx'
+ AND e.extname = 'btree_gist'
+ AND d.deptype = 'x';
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 37c6d730840..ea35aa591a8 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9e6a39f5608..50c3cc6f64c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2198,6 +2198,7 @@ PartitionDirectoryEntry
PartitionDispatch
PartitionElem
PartitionHashBound
+PartitionIndexExtDepEntry
PartitionKey
PartitionListValue
PartitionMap
--
2.52.0
Attachments:
[text/plain] v2-0001-Preserve-extension-dependencies-on-indexes-during.patch (17.9K, 2-v2-0001-Preserve-extension-dependencies-on-indexes-during.patch)
download | inline diff:
From 44c37e30e60b53d4478dbf32d4aae2a7d6b9b17f Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v2] 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 (created via
ALTER INDEX ... DEPENDS ON EXTENSION) 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.
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gmail.com
Reported-by: Kirill Reshke
---
src/backend/commands/tablecmds.c | 218 ++++++++++++++++++
src/test/regress/expected/partition_merge.out | 40 ++++
src/test/regress/expected/partition_split.out | 41 ++++
src/test/regress/sql/partition_merge.sql | 38 +++
src/test/regress/sql/partition_split.sql | 39 ++++
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 377 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index eec09ba1ded..afa89f8b3ce 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"
@@ -365,6 +366,16 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +771,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 +23014,171 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each index on the source partitions that has extension dependencies, we
+ * collect a mapping from the parent partitioned index OID to the list of
+ * extension OIDs.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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 parentIdxOid;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+ ListCell *lc;
+
+ /* Get the parent index if this is a partition index */
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Get extension dependencies for this index */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+ if (extDeps == NIL)
+ continue;
+
+ /*
+ * Look for existing entry for this parent index.
+ */
+ foreach(lc, result)
+ {
+ PartitionIndexExtDepEntry *e = lfirst(lc);
+
+ if (e->parentIndexOid == parentIdxOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (entry != NULL)
+ {
+ /* Add extension dependencies avoiding duplicates */
+ foreach_oid(extOid, extDeps)
+ {
+ if (!list_member_oid(entry->extensionOids, extOid))
+ entry->extensionOids = lappend_oid(entry->extensionOids,
+ extOid);
+ }
+ list_free(extDeps);
+ }
+ else
+ {
+ /* Create new entry */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIdxOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Get the parent index if this is a partition index */
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23188,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 +23278,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 +23362,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 +23665,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 +23704,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 +23793,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/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 925fe4f570a..00aa16964ce 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,46 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION if not exists btree_gist;
+CREATE TABLE t_merge_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+-- Add extension dependency on partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Verify the dependency exists in pg_depend
+SELECT COUNT(*) > 0 AS has_ext_dep
+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 = 't_merge_extdep_merged_i_idx'
+ AND e.extname = 'btree_gist'
+ AND d.deptype = 'x';
+ has_ext_dep
+-------------
+ t
+(1 row)
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 13ca733f9fa..a89c2dadc97 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..4864c66636d 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,44 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION if not exists btree_gist;
+
+CREATE TABLE t_merge_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+
+-- Add extension dependency on partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+
+-- Verify the dependency exists in pg_depend
+SELECT COUNT(*) > 0 AS has_ext_dep
+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 = 't_merge_extdep_merged_i_idx'
+ AND e.extname = 'btree_gist'
+ AND d.deptype = 'x';
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 37c6d730840..ea35aa591a8 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9e6a39f5608..50c3cc6f64c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2198,6 +2198,7 @@ PartitionDirectoryEntry
PartitionDispatch
PartitionElem
PartitionHashBound
+PartitionIndexExtDepEntry
PartitionKey
PartitionListValue
PartitionMap
--
2.52.0
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-14 09:05 Dmitry Koval <[email protected]>
parent: Matheus Alcantara <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Dmitry Koval @ 2026-04-14 09:05 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; pgsql-hackers
Hi Matheus!
Thank you for patch.
I agree that dependency should be automatically added for SPLIT
PARTITION. But I'm not sure about MERGE PARTITION ...
Might be it would be more correct to automatically add a dependency only
if all merged partitions have it?
--
With best regards,
Dmitry Koval
Postgres Professional: http://postgrespro.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-16 18:03 Matheus Alcantara <[email protected]>
parent: Dmitry Koval <[email protected]>
0 siblings, 2 replies; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-16 18:03 UTC (permalink / raw)
To: Dmitry Koval <[email protected]>; pgsql-hackers
On Tue Apr 14, 2026 at 6:05 AM -03, Dmitry Koval wrote:
> Hi Matheus!
>
> Thank you for patch.
> I agree that dependency should be automatically added for SPLIT
> PARTITION. But I'm not sure about MERGE PARTITION ...
> Might be it would be more correct to automatically add a dependency only
> if all merged partitions have it?
Hi,
Thank you for taking a look on this!
I agree with your suggestion. The attached patch implements the
intersection behavior for MERGE PARTITIONS: extension dependencies are
only preserved on the merged partition's index if all source partition
indexes have that dependency.
For example:
MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a) -- only ext_a is common
MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
For SPLIT PARTITION, the behavior remains the same since there's only
one source partition, all its extension dependencies are copied to the
new partition indexes.
While working on this patch, I noticed what might be a separate bug (or
perhaps intentional behavior that I don't understand): extension
dependencies on parent partitioned indexes don't seem to prevent DROP
EXTENSION, but dependencies on child partition indexes do. See this
example:
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE t (i int) PARTITION BY RANGE (i);
CREATE TABLE t_1 PARTITION OF t FOR VALUES FROM (1) TO (2);
CREATE INDEX t_idx ON t USING gist (i);
-- Add dependency on the PARENT partitioned index
ALTER INDEX t_idx DEPENDS ON EXTENSION btree_gist;
-- This succeeds (I expected it to fail):
DROP EXTENSION btree_gist;
But if I add the dependency on the child partition index instead:
ALTER INDEX t_1_i_idx DEPENDS ON EXTENSION btree_gist;
DROP EXTENSION btree_gist;
ERROR: cannot drop extension btree_gist because other objects depend on it
DETAIL: index t_idx depends on operator class gist_int4_ops for access method gist
Is this expected behavior, or a separate bug? I would have expected the
dependency on the parent index to also prevent the DROP.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From d89992ca128580953c8893d93ff573bb60b323a0 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v3] 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 (created via
ALTER INDEX ... DEPENDS ON EXTENSION) 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, only extension dependencies that exist on ALL
source partition indexes are preserved on the merged partition's index.
For example:
MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a)
MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.
Also adds list_intersection_oid() to support the intersection logic.
Author: Matheus Alcantara <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gma...
---
doc/src/sgml/ref/alter_table.sgml | 20 ++
src/backend/commands/tablecmds.c | 251 ++++++++++++++++++
src/backend/nodes/list.c | 26 ++
src/include/nodes/pg_list.h | 1 +
src/test/regress/expected/partition_merge.out | 105 ++++++++
src/test/regress/expected/partition_split.out | 41 +++
src/test/regress/sql/partition_merge.sql | 92 +++++++
src/test/regress/sql/partition_split.sql | 39 +++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 576 insertions(+)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 453395c5c73..5dde3ce57f1 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1255,6 +1255,18 @@ 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.
+ When merging partitions, only extension dependencies that exist on
+ <emphasis>all</emphasis> source partition indexes are preserved on
+ the merged partition's corresponding index. For example, if two
+ partitions are merged and only one partition's index depends on an
+ extension, that dependency will not be carried over to the merged
+ partition's index.
+ </para>
+
<note>
<para>
Merging partitions acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
@@ -1342,6 +1354,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..14adb089df5 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"
@@ -365,6 +366,19 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ *
+ * Extension dependencies are created on the new partition based
+ * on the indexes that share the same parent index oid.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +774,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 +23017,201 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. Dependencies are stored grouped by parent index OID.
+ *
+ * When multiple partitions have indexes with the same parent, only the
+ * intersection of their extension dependencies is kept. This ensures that
+ * after merge, the new partition's index only has dependencies that ALL
+ * source partition indexes had. For example:
+ *
+ * MERGE(INDEX1(EXT_A, EXT_B), INDEX2(EXT_A)) -> INDEX3(EXT_A)
+ * MERGE(INDEX1(EXT_A), INDEX2()) -> INDEX3() -- no common deps
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+
+ /* Get the parent index if this is a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIndexOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIndexOid))
+ continue;
+
+ /* Get extension dependencies for this index. */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+
+ /* Look for existing partition entry for this parent index */
+ foreach_ptr(PartitionIndexExtDepEntry, e, result)
+ {
+ if (e->parentIndexOid == parentIndexOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (extDeps == NIL)
+ {
+ /*
+ * This index has no extension dependencies. Since we only
+ * preserve dependencies that exist on ALL source partition
+ * indexes, finding one index without deps means the result
+ * for this parent index must be empty.
+ */
+ if (entry != NULL)
+ {
+ list_free(entry->extensionOids);
+ entry->extensionOids = NIL;
+ }
+ /* No need to create an entry for no deps if none exists */
+ }
+ else if (entry != NULL)
+ {
+ /*
+ * We already have an entry for this parent index from a
+ * previous partition. Compute the intersection to keep only
+ * deps that exist on both.
+ */
+ List *old = entry->extensionOids;
+
+ entry->extensionOids = list_intersection_oid(old, extDeps);
+ list_free(old);
+ list_free(extDeps);
+ }
+ else
+ {
+ /*
+ * First partition index we've seen for this parent. Create a
+ * new entry with all its dependencies - subsequent partitions
+ * will narrow this down via intersection.
+ */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIndexOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Skip if this is not a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23221,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 +23311,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 +23395,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 +23698,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 +23737,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 +23826,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/backend/nodes/list.c b/src/backend/nodes/list.c
index 98fc2b44b50..0af633de130 100644
--- a/src/backend/nodes/list.c
+++ b/src/backend/nodes/list.c
@@ -1219,6 +1219,32 @@ list_intersection_int(const List *list1, const List *list2)
return result;
}
+/*
+ * As list_intersection but operates on lists of oids.
+ */
+List *
+list_intersection_oid(const List *list1, const List *list2)
+{
+ List *result;
+ const ListCell *cell;
+
+ if (list1 == NIL || list2 == NIL)
+ return NIL;
+
+ Assert(IsOidList(list1));
+ Assert(IsOidList(list2));
+
+ result = NIL;
+ foreach(cell, list1)
+ {
+ if (list_member_oid(list2, lfirst_oid(cell)))
+ result = lappend_oid(result, lfirst_oid(cell));
+ }
+
+ check_list_invariants(result);
+ return result;
+}
+
/*
* Return a list that contains all the cells in list1 that are not in
* list2. The returned list is freshly allocated via palloc(), but the
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index e93bcbf2698..e0f29aa1139 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -681,6 +681,7 @@ extern List *list_union_oid(const List *list1, const List *list2);
extern List *list_intersection(const List *list1, const List *list2);
extern List *list_intersection_int(const List *list1, const List *list2);
+extern List *list_intersection_oid(const List *list1, const List *list2);
/* currently, there's no need for list_intersection_ptr etc */
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 883110e25d9..b29f5f2c764 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,111 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS btree_gist;
+CREATE EXTENSION IF NOT EXISTS btree_gin;
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep USING gist (x);
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION btree_gin;
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION btree_gin;
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+index t_merge_extdep_x depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+index t_merge_extdep_x depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('btree_gist', 'btree_gin')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+-----------------------------+------------
+ t_merge_extdep_merged_i_idx | btree_gist
+ t_merge_extdep_merged_x_idx | btree_gin
+(2 rows)
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x';
+ extname
+---------
+(0 rows)
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+------------------------------+-----------
+ t_merge_extdep_merged3_x_idx | btree_gin
+(1 row)
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5 USING gist(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+ relname
+------------------------------
+ t_merge_extdep_merged4_i_idx
+ t_merge_extdep_merged4_x_idx
+(2 rows)
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
+DROP EXTENSION btree_gin;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 43ca299648e..574119c11a7 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..e01378452bf 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,98 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS btree_gist;
+CREATE EXTENSION IF NOT EXISTS btree_gin;
+
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep USING gist (x);
+
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION btree_gin;
+
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION btree_gin;
+
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('btree_gist', 'btree_gin')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x';
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5 USING gist(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
+DROP EXTENSION btree_gin;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 44fcf208ac6..22563d3804c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
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.53.0
Attachments:
[text/plain] v3-0001-Preserve-extension-dependencies-on-indexes-during.patch (29.3K, 2-v3-0001-Preserve-extension-dependencies-on-indexes-during.patch)
download | inline diff:
From d89992ca128580953c8893d93ff573bb60b323a0 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v3] 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 (created via
ALTER INDEX ... DEPENDS ON EXTENSION) 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, only extension dependencies that exist on ALL
source partition indexes are preserved on the merged partition's index.
For example:
MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a)
MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.
Also adds list_intersection_oid() to support the intersection logic.
Author: Matheus Alcantara <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gmail.com
---
doc/src/sgml/ref/alter_table.sgml | 20 ++
src/backend/commands/tablecmds.c | 251 ++++++++++++++++++
src/backend/nodes/list.c | 26 ++
src/include/nodes/pg_list.h | 1 +
src/test/regress/expected/partition_merge.out | 105 ++++++++
src/test/regress/expected/partition_split.out | 41 +++
src/test/regress/sql/partition_merge.sql | 92 +++++++
src/test/regress/sql/partition_split.sql | 39 +++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 576 insertions(+)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 453395c5c73..5dde3ce57f1 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1255,6 +1255,18 @@ 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.
+ When merging partitions, only extension dependencies that exist on
+ <emphasis>all</emphasis> source partition indexes are preserved on
+ the merged partition's corresponding index. For example, if two
+ partitions are merged and only one partition's index depends on an
+ extension, that dependency will not be carried over to the merged
+ partition's index.
+ </para>
+
<note>
<para>
Merging partitions acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
@@ -1342,6 +1354,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..14adb089df5 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"
@@ -365,6 +366,19 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ *
+ * Extension dependencies are created on the new partition based
+ * on the indexes that share the same parent index oid.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +774,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 +23017,201 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. Dependencies are stored grouped by parent index OID.
+ *
+ * When multiple partitions have indexes with the same parent, only the
+ * intersection of their extension dependencies is kept. This ensures that
+ * after merge, the new partition's index only has dependencies that ALL
+ * source partition indexes had. For example:
+ *
+ * MERGE(INDEX1(EXT_A, EXT_B), INDEX2(EXT_A)) -> INDEX3(EXT_A)
+ * MERGE(INDEX1(EXT_A), INDEX2()) -> INDEX3() -- no common deps
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+
+ /* Get the parent index if this is a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIndexOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIndexOid))
+ continue;
+
+ /* Get extension dependencies for this index. */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+
+ /* Look for existing partition entry for this parent index */
+ foreach_ptr(PartitionIndexExtDepEntry, e, result)
+ {
+ if (e->parentIndexOid == parentIndexOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (extDeps == NIL)
+ {
+ /*
+ * This index has no extension dependencies. Since we only
+ * preserve dependencies that exist on ALL source partition
+ * indexes, finding one index without deps means the result
+ * for this parent index must be empty.
+ */
+ if (entry != NULL)
+ {
+ list_free(entry->extensionOids);
+ entry->extensionOids = NIL;
+ }
+ /* No need to create an entry for no deps if none exists */
+ }
+ else if (entry != NULL)
+ {
+ /*
+ * We already have an entry for this parent index from a
+ * previous partition. Compute the intersection to keep only
+ * deps that exist on both.
+ */
+ List *old = entry->extensionOids;
+
+ entry->extensionOids = list_intersection_oid(old, extDeps);
+ list_free(old);
+ list_free(extDeps);
+ }
+ else
+ {
+ /*
+ * First partition index we've seen for this parent. Create a
+ * new entry with all its dependencies - subsequent partitions
+ * will narrow this down via intersection.
+ */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIndexOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Skip if this is not a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23221,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 +23311,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 +23395,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 +23698,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 +23737,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 +23826,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/backend/nodes/list.c b/src/backend/nodes/list.c
index 98fc2b44b50..0af633de130 100644
--- a/src/backend/nodes/list.c
+++ b/src/backend/nodes/list.c
@@ -1219,6 +1219,32 @@ list_intersection_int(const List *list1, const List *list2)
return result;
}
+/*
+ * As list_intersection but operates on lists of oids.
+ */
+List *
+list_intersection_oid(const List *list1, const List *list2)
+{
+ List *result;
+ const ListCell *cell;
+
+ if (list1 == NIL || list2 == NIL)
+ return NIL;
+
+ Assert(IsOidList(list1));
+ Assert(IsOidList(list2));
+
+ result = NIL;
+ foreach(cell, list1)
+ {
+ if (list_member_oid(list2, lfirst_oid(cell)))
+ result = lappend_oid(result, lfirst_oid(cell));
+ }
+
+ check_list_invariants(result);
+ return result;
+}
+
/*
* Return a list that contains all the cells in list1 that are not in
* list2. The returned list is freshly allocated via palloc(), but the
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index e93bcbf2698..e0f29aa1139 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -681,6 +681,7 @@ extern List *list_union_oid(const List *list1, const List *list2);
extern List *list_intersection(const List *list1, const List *list2);
extern List *list_intersection_int(const List *list1, const List *list2);
+extern List *list_intersection_oid(const List *list1, const List *list2);
/* currently, there's no need for list_intersection_ptr etc */
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 883110e25d9..b29f5f2c764 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,111 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS btree_gist;
+CREATE EXTENSION IF NOT EXISTS btree_gin;
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep USING gist (x);
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION btree_gin;
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION btree_gin;
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+index t_merge_extdep_x depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+ERROR: cannot drop extension btree_gist because other objects depend on it
+DETAIL: index t_merge_extdep_idx depends on operator class gist_int4_ops for access method gist
+index t_merge_extdep_x depends on operator class gist_int4_ops for access method gist
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('btree_gist', 'btree_gin')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+-----------------------------+------------
+ t_merge_extdep_merged_i_idx | btree_gist
+ t_merge_extdep_merged_x_idx | btree_gin
+(2 rows)
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x';
+ extname
+---------
+(0 rows)
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+------------------------------+-----------
+ t_merge_extdep_merged3_x_idx | btree_gin
+(1 row)
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5 USING gist(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+ relname
+------------------------------
+ t_merge_extdep_merged4_i_idx
+ t_merge_extdep_merged4_x_idx
+(2 rows)
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
+DROP EXTENSION btree_gin;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 43ca299648e..574119c11a7 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..e01378452bf 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,98 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS btree_gist;
+CREATE EXTENSION IF NOT EXISTS btree_gin;
+
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep USING gist (i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep USING gist (x);
+
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION btree_gin;
+
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION btree_gin;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION btree_gin;
+
+-- Should fail: dependencies exist
+DROP EXTENSION btree_gist;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION btree_gist;
+
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('btree_gist', 'btree_gin')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x';
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION btree_gist;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION btree_gin;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('btree_gin', 'btree_gist')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5 USING gist(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION btree_gist;
+DROP EXTENSION btree_gin;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 44fcf208ac6..22563d3804c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
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.53.0
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-20 00:04 Dmitry Koval <[email protected]>
parent: Matheus Alcantara <[email protected]>
1 sibling, 1 reply; 21+ messages in thread
From: Dmitry Koval @ 2026-04-20 00:04 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; pgsql-hackers
Hi Matheus!
>v3-0001-Preserve-extension-dependencies-on-indexes-during.patch
I looked patch and did not find problems. But there is one point:
extensions btree_gist, btree_gin, citext are not included by default.
So command
> ./configure --enable-debug --enable-cassert --prefix `pwd`/install
> >/dev/null && make -s && make install -s && make check
generates errors like
ERROR: extension "btree_gist" is not available
ERROR: extension "btree_gin" is not available
ERROR: extension "citext" is not available
Might be it would be better to use for tests extensions from the
catalog src/test/modules/test_extensions (see test
src/test/modules/test_extensions/sql/test_extdepend.sql)?
>extension dependencies on parent partitioned indexes don't seem to
>prevent DROP EXTENSION, but dependencies on child partition indexes
>do. ...
I agree, it looks strange ...
With best regards,
Dmitry Koval
Postgres Professional: http://postgrespro.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-20 15:23 Matheus Alcantara <[email protected]>
parent: Dmitry Koval <[email protected]>
0 siblings, 0 replies; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-20 15:23 UTC (permalink / raw)
To: Dmitry Koval <[email protected]>; pgsql-hackers
On Sun Apr 19, 2026 at 9:04 PM -03, Dmitry Koval wrote:
> Hi Matheus!
>
> >v3-0001-Preserve-extension-dependencies-on-indexes-during.patch
>
> I looked patch and did not find problems. But there is one point:
> extensions btree_gist, btree_gin, citext are not included by default.
> So command
>
> > ./configure --enable-debug --enable-cassert --prefix `pwd`/install
> > >/dev/null && make -s && make install -s && make check
>
> generates errors like
>
> ERROR: extension "btree_gist" is not available
> ERROR: extension "btree_gin" is not available
> ERROR: extension "citext" is not available
>
> Might be it would be better to use for tests extensions from the
> catalog src/test/modules/test_extensions (see test
> src/test/modules/test_extensions/sql/test_extdepend.sql)?
>
Thanks for looking at this!
Yeah, some build farm animals will not be happy with these new tests.
Fixed on new attached v4 to use extensions from
src/test/modules/test_extensions.
> >extension dependencies on parent partitioned indexes don't seem to
> >prevent DROP EXTENSION, but dependencies on child partition indexes
> >do. ...
>
> I agree, it looks strange ...
>
I'll start a new thread to discuss this.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From 28b224f86e1791da635d51b7c8a4f8b04ded7cee Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v4] 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 (created via
ALTER INDEX ... DEPENDS ON EXTENSION) 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, only extension dependencies that exist on ALL
source partition indexes are preserved on the merged partition's index.
For example:
MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a)
MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.
Also adds list_intersection_oid() to support the intersection logic.
Author: Matheus Alcantara <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gma...
---
doc/src/sgml/ref/alter_table.sgml | 20 ++
src/backend/commands/tablecmds.c | 251 ++++++++++++++++++
src/backend/nodes/list.c | 26 ++
src/include/nodes/pg_list.h | 1 +
src/test/regress/expected/partition_merge.out | 101 +++++++
src/test/regress/expected/partition_split.out | 41 +++
src/test/regress/sql/partition_merge.sql | 92 +++++++
src/test/regress/sql/partition_split.sql | 39 +++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 572 insertions(+)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 453395c5c73..5dde3ce57f1 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1255,6 +1255,18 @@ 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.
+ When merging partitions, only extension dependencies that exist on
+ <emphasis>all</emphasis> source partition indexes are preserved on
+ the merged partition's corresponding index. For example, if two
+ partitions are merged and only one partition's index depends on an
+ extension, that dependency will not be carried over to the merged
+ partition's index.
+ </para>
+
<note>
<para>
Merging partitions acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
@@ -1342,6 +1354,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..14adb089df5 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"
@@ -365,6 +366,19 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ *
+ * Extension dependencies are created on the new partition based
+ * on the indexes that share the same parent index oid.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +774,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 +23017,201 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. Dependencies are stored grouped by parent index OID.
+ *
+ * When multiple partitions have indexes with the same parent, only the
+ * intersection of their extension dependencies is kept. This ensures that
+ * after merge, the new partition's index only has dependencies that ALL
+ * source partition indexes had. For example:
+ *
+ * MERGE(INDEX1(EXT_A, EXT_B), INDEX2(EXT_A)) -> INDEX3(EXT_A)
+ * MERGE(INDEX1(EXT_A), INDEX2()) -> INDEX3() -- no common deps
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+
+ /* Get the parent index if this is a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIndexOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIndexOid))
+ continue;
+
+ /* Get extension dependencies for this index. */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+
+ /* Look for existing partition entry for this parent index */
+ foreach_ptr(PartitionIndexExtDepEntry, e, result)
+ {
+ if (e->parentIndexOid == parentIndexOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (extDeps == NIL)
+ {
+ /*
+ * This index has no extension dependencies. Since we only
+ * preserve dependencies that exist on ALL source partition
+ * indexes, finding one index without deps means the result
+ * for this parent index must be empty.
+ */
+ if (entry != NULL)
+ {
+ list_free(entry->extensionOids);
+ entry->extensionOids = NIL;
+ }
+ /* No need to create an entry for no deps if none exists */
+ }
+ else if (entry != NULL)
+ {
+ /*
+ * We already have an entry for this parent index from a
+ * previous partition. Compute the intersection to keep only
+ * deps that exist on both.
+ */
+ List *old = entry->extensionOids;
+
+ entry->extensionOids = list_intersection_oid(old, extDeps);
+ list_free(old);
+ list_free(extDeps);
+ }
+ else
+ {
+ /*
+ * First partition index we've seen for this parent. Create a
+ * new entry with all its dependencies - subsequent partitions
+ * will narrow this down via intersection.
+ */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIndexOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Skip if this is not a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23221,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 +23311,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 +23395,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 +23698,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 +23737,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 +23826,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/backend/nodes/list.c b/src/backend/nodes/list.c
index 98fc2b44b50..0af633de130 100644
--- a/src/backend/nodes/list.c
+++ b/src/backend/nodes/list.c
@@ -1219,6 +1219,32 @@ list_intersection_int(const List *list1, const List *list2)
return result;
}
+/*
+ * As list_intersection but operates on lists of oids.
+ */
+List *
+list_intersection_oid(const List *list1, const List *list2)
+{
+ List *result;
+ const ListCell *cell;
+
+ if (list1 == NIL || list2 == NIL)
+ return NIL;
+
+ Assert(IsOidList(list1));
+ Assert(IsOidList(list2));
+
+ result = NIL;
+ foreach(cell, list1)
+ {
+ if (list_member_oid(list2, lfirst_oid(cell)))
+ result = lappend_oid(result, lfirst_oid(cell));
+ }
+
+ check_list_invariants(result);
+ return result;
+}
+
/*
* Return a list that contains all the cells in list1 that are not in
* list2. The returned list is freshly allocated via palloc(), but the
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index e93bcbf2698..e0f29aa1139 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -681,6 +681,7 @@ extern List *list_union_oid(const List *list1, const List *list2);
extern List *list_intersection(const List *list1, const List *list2);
extern List *list_intersection_int(const List *list1, const List *list2);
+extern List *list_intersection_oid(const List *list1, const List *list2);
/* currently, there's no need for list_intersection_ptr etc */
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 883110e25d9..5bd40c14a7e 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,107 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_2_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_merged_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+-----------------------------+-----------
+ t_merge_extdep_merged_i_idx | test_ext3
+ t_merge_extdep_merged_x_idx | test_ext5
+(2 rows)
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x';
+ extname
+---------
+(0 rows)
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+------------------------------+-----------
+ t_merge_extdep_merged3_x_idx | test_ext5
+(1 row)
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+ relname
+------------------------------
+ t_merge_extdep_merged4_i_idx
+ t_merge_extdep_merged4_x_idx
+(2 rows)
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 43ca299648e..574119c11a7 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..66fc85ba950 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,98 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x';
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 44fcf208ac6..22563d3804c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
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.53.0
Attachments:
[text/plain] v4-0001-Preserve-extension-dependencies-on-indexes-during.patch (28.9K, 2-v4-0001-Preserve-extension-dependencies-on-indexes-during.patch)
download | inline diff:
From 28b224f86e1791da635d51b7c8a4f8b04ded7cee Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v4] 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 (created via
ALTER INDEX ... DEPENDS ON EXTENSION) 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, only extension dependencies that exist on ALL
source partition indexes are preserved on the merged partition's index.
For example:
MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a)
MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.
Also adds list_intersection_oid() to support the intersection logic.
Author: Matheus Alcantara <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gmail.com
---
doc/src/sgml/ref/alter_table.sgml | 20 ++
src/backend/commands/tablecmds.c | 251 ++++++++++++++++++
src/backend/nodes/list.c | 26 ++
src/include/nodes/pg_list.h | 1 +
src/test/regress/expected/partition_merge.out | 101 +++++++
src/test/regress/expected/partition_split.out | 41 +++
src/test/regress/sql/partition_merge.sql | 92 +++++++
src/test/regress/sql/partition_split.sql | 39 +++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 572 insertions(+)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 453395c5c73..5dde3ce57f1 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1255,6 +1255,18 @@ 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.
+ When merging partitions, only extension dependencies that exist on
+ <emphasis>all</emphasis> source partition indexes are preserved on
+ the merged partition's corresponding index. For example, if two
+ partitions are merged and only one partition's index depends on an
+ extension, that dependency will not be carried over to the merged
+ partition's index.
+ </para>
+
<note>
<para>
Merging partitions acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
@@ -1342,6 +1354,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..14adb089df5 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"
@@ -365,6 +366,19 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ *
+ * Extension dependencies are created on the new partition based
+ * on the indexes that share the same parent index oid.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +774,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 +23017,201 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. Dependencies are stored grouped by parent index OID.
+ *
+ * When multiple partitions have indexes with the same parent, only the
+ * intersection of their extension dependencies is kept. This ensures that
+ * after merge, the new partition's index only has dependencies that ALL
+ * source partition indexes had. For example:
+ *
+ * MERGE(INDEX1(EXT_A, EXT_B), INDEX2(EXT_A)) -> INDEX3(EXT_A)
+ * MERGE(INDEX1(EXT_A), INDEX2()) -> INDEX3() -- no common deps
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+
+ /* Get the parent index if this is a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIndexOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIndexOid))
+ continue;
+
+ /* Get extension dependencies for this index. */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+
+ /* Look for existing partition entry for this parent index */
+ foreach_ptr(PartitionIndexExtDepEntry, e, result)
+ {
+ if (e->parentIndexOid == parentIndexOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (extDeps == NIL)
+ {
+ /*
+ * This index has no extension dependencies. Since we only
+ * preserve dependencies that exist on ALL source partition
+ * indexes, finding one index without deps means the result
+ * for this parent index must be empty.
+ */
+ if (entry != NULL)
+ {
+ list_free(entry->extensionOids);
+ entry->extensionOids = NIL;
+ }
+ /* No need to create an entry for no deps if none exists */
+ }
+ else if (entry != NULL)
+ {
+ /*
+ * We already have an entry for this parent index from a
+ * previous partition. Compute the intersection to keep only
+ * deps that exist on both.
+ */
+ List *old = entry->extensionOids;
+
+ entry->extensionOids = list_intersection_oid(old, extDeps);
+ list_free(old);
+ list_free(extDeps);
+ }
+ else
+ {
+ /*
+ * First partition index we've seen for this parent. Create a
+ * new entry with all its dependencies - subsequent partitions
+ * will narrow this down via intersection.
+ */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIndexOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Skip if this is not a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23221,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 +23311,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 +23395,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 +23698,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 +23737,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 +23826,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/backend/nodes/list.c b/src/backend/nodes/list.c
index 98fc2b44b50..0af633de130 100644
--- a/src/backend/nodes/list.c
+++ b/src/backend/nodes/list.c
@@ -1219,6 +1219,32 @@ list_intersection_int(const List *list1, const List *list2)
return result;
}
+/*
+ * As list_intersection but operates on lists of oids.
+ */
+List *
+list_intersection_oid(const List *list1, const List *list2)
+{
+ List *result;
+ const ListCell *cell;
+
+ if (list1 == NIL || list2 == NIL)
+ return NIL;
+
+ Assert(IsOidList(list1));
+ Assert(IsOidList(list2));
+
+ result = NIL;
+ foreach(cell, list1)
+ {
+ if (list_member_oid(list2, lfirst_oid(cell)))
+ result = lappend_oid(result, lfirst_oid(cell));
+ }
+
+ check_list_invariants(result);
+ return result;
+}
+
/*
* Return a list that contains all the cells in list1 that are not in
* list2. The returned list is freshly allocated via palloc(), but the
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index e93bcbf2698..e0f29aa1139 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -681,6 +681,7 @@ extern List *list_union_oid(const List *list1, const List *list2);
extern List *list_intersection(const List *list1, const List *list2);
extern List *list_intersection_int(const List *list1, const List *list2);
+extern List *list_intersection_oid(const List *list1, const List *list2);
/* currently, there's no need for list_intersection_ptr etc */
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 883110e25d9..5bd40c14a7e 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,107 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_2_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_merged_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+-----------------------------+-----------
+ t_merge_extdep_merged_i_idx | test_ext3
+ t_merge_extdep_merged_x_idx | test_ext5
+(2 rows)
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x';
+ extname
+---------
+(0 rows)
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+------------------------------+-----------
+ t_merge_extdep_merged3_x_idx | test_ext5
+(1 row)
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+ relname
+------------------------------
+ t_merge_extdep_merged4_i_idx
+ t_merge_extdep_merged4_x_idx
+(2 rows)
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 43ca299648e..574119c11a7 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..66fc85ba950 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,98 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4); -- Don't have a dependency
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6);
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Add a different extension dependency to ensure that only indexes with the
+-- same dependencies has the dependencies preserved.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_4_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Merge a partition with a dependency with a partition that don't have a
+-- dependency. The new merged partition should not have a dependency.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+
+SELECT 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 ('t_merge_extdep_merged2_i_idx', 't_merge_extdep_merged2_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x';
+
+-- Merge partitions with different dependencies and check that the new merged
+-- partition only have the dependencies that intersect.
+ALTER INDEX t_merge_extdep_merged2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_merged2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+
+SELECT relname, 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 ('t_merge_extdep_merged3_i_idx', 't_merge_extdep_merged3_x_idx')
+ AND e.extname IN ('test_ext5', 'test_ext3')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_5_y_idx ON t_merge_extdep_5(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged3, t_merge_extdep_5) INTO t_merge_extdep_merged4;
+
+-- The partition-only index is dropped with its partition; no corresponding
+-- index exists on the merged partition since there's no parent index.
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged4%idx'
+ORDER BY relname;
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 44fcf208ac6..22563d3804c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
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.53.0
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-20 19:08 Alexander Korotkov <[email protected]>
parent: Matheus Alcantara <[email protected]>
1 sibling, 1 reply; 21+ messages in thread
From: Alexander Korotkov @ 2026-04-20 19:08 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On Thu, Apr 16, 2026 at 9:03 PM Matheus Alcantara
<[email protected]> wrote:
>
> On Tue Apr 14, 2026 at 6:05 AM -03, Dmitry Koval wrote:
> > Hi Matheus!
> >
> > Thank you for patch.
> > I agree that dependency should be automatically added for SPLIT
> > PARTITION. But I'm not sure about MERGE PARTITION ...
> > Might be it would be more correct to automatically add a dependency only
> > if all merged partitions have it?
>
> Hi,
>
> Thank you for taking a look on this!
>
> I agree with your suggestion. The attached patch implements the
> intersection behavior for MERGE PARTITIONS: extension dependencies are
> only preserved on the merged partition's index if all source partition
> indexes have that dependency.
>
> For example:
> MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a) -- only ext_a is common
> MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
This is not obvious for me. I would rather trigger an error if there
are different dependencies on merging partitions.
> For SPLIT PARTITION, the behavior remains the same since there's only
> one source partition, all its extension dependencies are copied to the
> new partition indexes.
>
> While working on this patch, I noticed what might be a separate bug (or
> perhaps intentional behavior that I don't understand): extension
> dependencies on parent partitioned indexes don't seem to prevent DROP
> EXTENSION, but dependencies on child partition indexes do. See this
> example:
>
> CREATE EXTENSION IF NOT EXISTS btree_gist;
>
> CREATE TABLE t (i int) PARTITION BY RANGE (i);
> CREATE TABLE t_1 PARTITION OF t FOR VALUES FROM (1) TO (2);
> CREATE INDEX t_idx ON t USING gist (i);
>
> -- Add dependency on the PARENT partitioned index
> ALTER INDEX t_idx DEPENDS ON EXTENSION btree_gist;
>
> -- This succeeds (I expected it to fail):
> DROP EXTENSION btree_gist;
>
> But if I add the dependency on the child partition index instead:
>
> ALTER INDEX t_1_i_idx DEPENDS ON EXTENSION btree_gist;
>
> DROP EXTENSION btree_gist;
> ERROR: cannot drop extension btree_gist because other objects depend on it
> DETAIL: index t_idx depends on operator class gist_int4_ops for access method gist
>
> Is this expected behavior, or a separate bug? I would have expected the
> dependency on the parent index to also prevent the DROP.
Note that if you don't create explicit dependency then you would get
an error. When you create an explicit dependency index => extension,
it's a different kind than the automatic one. It causes index to be
dropped on extension drop (as documented). Thus, if you create
dependency child_index => extension, then child_index gets dropped,
but parent_index automatic dependency still prevents deletion. But if
you create dependency parent_index => extension then dropping of
extension trigger dropping of the parent_index, in turn that dropping
child_index. So, everything is dropped, no errors. I don't think
there is a bug.
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-21 13:20 Matheus Alcantara <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-21 13:20 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On Mon Apr 20, 2026 at 4:08 PM -03, Alexander Korotkov wrote:
> On Thu, Apr 16, 2026 at 9:03 PM Matheus Alcantara
> <[email protected]> wrote:
>>
>> On Tue Apr 14, 2026 at 6:05 AM -03, Dmitry Koval wrote:
>> > Hi Matheus!
>> >
>> > Thank you for patch.
>> > I agree that dependency should be automatically added for SPLIT
>> > PARTITION. But I'm not sure about MERGE PARTITION ...
>> > Might be it would be more correct to automatically add a dependency only
>> > if all merged partitions have it?
>>
>> Hi,
>>
>> Thank you for taking a look on this!
>>
>> I agree with your suggestion. The attached patch implements the
>> intersection behavior for MERGE PARTITIONS: extension dependencies are
>> only preserved on the merged partition's index if all source partition
>> indexes have that dependency.
>>
>> For example:
>> MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a) -- only ext_a is common
>> MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
>
> This is not obvious for me. I would rather trigger an error if there
> are different dependencies on merging partitions.
>
Yeah, I agree that this sounds a bit confusing, although this behavior
is documented on the last patch version I think that raising an error is
more simple and maybe is more obvious. The attached patch implement
this.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From 14fdb594d2ea3a2ccaf86a7243c62cc7a030ecba Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v5] 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 must have the same
extension dependencies; if they differ, an error is raised. This
ensures that extension dependencies are not silently lost during merge.
For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.
Author: Matheus Alcantara <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Reviewed-by: Alexander Korotkov <[email protected]>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gma...
---
doc/src/sgml/ref/alter_table.sgml | 17 ++
src/backend/commands/tablecmds.c | 255 ++++++++++++++++++
src/test/regress/expected/partition_merge.out | 86 ++++++
src/test/regress/expected/partition_split.out | 41 +++
src/test/regress/sql/partition_merge.sql | 76 ++++++
src/test/regress/sql/partition_split.sql | 39 +++
src/tools/pgindent/typedefs.list | 1 +
7 files changed, 515 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..1e2b2fc2cb0 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,19 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ *
+ * Extension dependencies are created on the new partition based
+ * on the indexes that share the same parent index oid.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +775,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 +23018,204 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. Dependencies are stored grouped by parent index OID.
+ *
+ * When multiple partitions have indexes with the same parent, all source
+ * partition indexes must have the same extension dependencies. If there is
+ * a mismatch (e.g., one partition's index depends on an extension while
+ * another doesn't, or they depend on different extensions), an error is
+ * raised. This ensures consistent behavior and avoids silently dropping
+ * dependencies during merge operations.
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+
+ /* Get the parent index if this is a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIndexOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIndexOid))
+ continue;
+
+ /* Get extension dependencies for this index. */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+
+ /* Look for existing partition entry for this parent index */
+ foreach_ptr(PartitionIndexExtDepEntry, e, result)
+ {
+ if (e->parentIndexOid == parentIndexOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (entry != NULL)
+ {
+ /*
+ * We already have an entry for this parent index from a
+ * previous partition. Verify that the dependencies match
+ * exactly.
+ */
+ if (extDeps == NIL && entry->extensionOids != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot merge partitions with conflicting extension dependencies"),
+ errdetail("Partition index \"%s\" has no extension dependencies, but another partition's index does.",
+ get_rel_name(indexOid))));
+
+ if (extDeps != NIL && entry->extensionOids == NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot merge partitions with conflicting extension dependencies"),
+ errdetail("Partition index \"%s\" has extension dependencies, but another partition's index does not.",
+ get_rel_name(indexOid))));
+
+ foreach_oid(extOid, extDeps)
+ {
+ if (!list_member_oid(entry->extensionOids, extOid))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot merge partitions with conflicting extension dependencies"),
+ errdetail("Partition index \"%s\" depends on extension \"%s\", but another partition's index does not.",
+ get_rel_name(indexOid),
+ get_extension_name(extOid))));
+ }
+ }
+ else
+ {
+ /*
+ * First partition index we've seen for this parent. Create a
+ * new entry with its dependencies (may be NIL) - subsequent
+ * partitions will be checked against this.
+ */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIndexOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Skip if this is not a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23225,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 +23315,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 +23399,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 +23702,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 +23741,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 +23830,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/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 883110e25d9..541f88fdbb1 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,92 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4);
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6); -- Don't have a dependency
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Add only a single dependency to test that it fails when merge partition with
+-- different extension dependencies.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_3_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_merged_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+-----------------------------+-----------
+ t_merge_extdep_merged_i_idx | test_ext3
+ t_merge_extdep_merged_x_idx | test_ext5
+(2 rows)
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_3_y_idx ON t_merge_extdep_3(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+-- The partition-only index is dropped with its partition
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged2%idx'
+ORDER BY relname;
+ relname
+------------------------------
+ t_merge_extdep_merged2_i_idx
+ t_merge_extdep_merged2_x_idx
+(2 rows)
+
+-- Should fail: Partitions to be merged have different extension dependencies.
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_4_i_idx" depends on extension "test_ext5", but another partition's index does not.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_merged2) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_merged2_i_idx" depends on extension "test_ext3", but another partition's index does not.
+-- Should fail: Partitions to be merged have different extension dependencies
+-- (t_merge_extdep_5 don't have dependencies)
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_5) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_5_i_idx" has no extension dependencies, but another partition's index does.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_5, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_4_i_idx" has extension dependencies, but another partition's index does not.
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 43ca299648e..574119c11a7 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..0f62129dcc1 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,82 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4);
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6); -- Don't have a dependency
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Add only a single dependency to test that it fails when merge partition with
+-- different extension dependencies.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_3_y_idx ON t_merge_extdep_3(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+
+-- The partition-only index is dropped with its partition
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged2%idx'
+ORDER BY relname;
+
+-- Should fail: Partitions to be merged have different extension dependencies.
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_merged2) INTO t_merge_extdep_merged3;
+
+-- Should fail: Partitions to be merged have different extension dependencies
+-- (t_merge_extdep_5 don't have dependencies)
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_5) INTO t_merge_extdep_merged3;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_5, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 44fcf208ac6..22563d3804c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
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.53.0
Attachments:
[text/plain] v5-0001-Preserve-extension-dependencies-on-indexes-during.patch (27.5K, 2-v5-0001-Preserve-extension-dependencies-on-indexes-during.patch)
download | inline diff:
From 14fdb594d2ea3a2ccaf86a7243c62cc7a030ecba Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v5] 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 must have the same
extension dependencies; if they differ, an error is raised. This
ensures that extension dependencies are not silently lost during merge.
For SPLIT operations, the new partition indexes simply inherit all
extension dependencies from the source partition's index.
Author: Matheus Alcantara <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Reviewed-by: Alexander Korotkov <[email protected]>
Discussion: https://www.postgresql.org/message-id/CALdSSPjXtzGM7Uk4fWRwRMXcCczge5uNirPQcYCHKPAWPkp9iQ%40mail.gmail.com
---
doc/src/sgml/ref/alter_table.sgml | 17 ++
src/backend/commands/tablecmds.c | 255 ++++++++++++++++++
src/test/regress/expected/partition_merge.out | 86 ++++++
src/test/regress/expected/partition_split.out | 41 +++
src/test/regress/sql/partition_merge.sql | 76 ++++++
src/test/regress/sql/partition_split.sql | 39 +++
src/tools/pgindent/typedefs.list | 1 +
7 files changed, 515 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..1e2b2fc2cb0 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,19 @@ typedef enum addFkConstraintSides
addFkBothSides,
} addFkConstraintSides;
+/*
+ * Hold extension dependencies for a partitioned index. Used by
+ * collectPartitionIndexExtDeps and applyPartitionIndexExtDeps.
+ *
+ * Extension dependencies are created on the new partition based
+ * on the indexes that share the same parent index oid.
+ */
+typedef struct PartitionIndexExtDepEntry
+{
+ Oid parentIndexOid; /* OID of the parent partitioned index */
+ List *extensionOids; /* List of extension OIDs this index depends */
+} PartitionIndexExtDepEntry;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -760,6 +775,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 +23018,204 @@ detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid
PopActiveSnapshot();
}
+/*
+ * collectPartitionIndexExtDeps: collect extension dependencies from indexes
+ * on the given partitions.
+ *
+ * For each partition index that has a parent partitioned index, we collect
+ * extension dependencies. Dependencies are stored grouped by parent index OID.
+ *
+ * When multiple partitions have indexes with the same parent, all source
+ * partition indexes must have the same extension dependencies. If there is
+ * a mismatch (e.g., one partition's index depends on an extension while
+ * another doesn't, or they depend on different extensions), an error is
+ * raised. This ensures consistent behavior and avoids silently dropping
+ * dependencies during merge operations.
+ *
+ * Indexes that don't have a parent partitioned index (i.e., indexes created
+ * directly on a partition without a corresponding parent index) are skipped.
+ *
+ * Returns a list of PartitionIndexExtDepEntry structs.
+ */
+static List *
+collectPartitionIndexExtDeps(List *partitionOids)
+{
+ List *result = NIL;
+
+ 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;
+ List *extDeps;
+ PartitionIndexExtDepEntry *entry = NULL;
+
+ /* Get the parent index if this is a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIndexOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIndexOid))
+ continue;
+
+ /* Get extension dependencies for this index. */
+ extDeps = getAutoExtensionsOfObject(RelationRelationId, indexOid);
+
+ /* Look for existing partition entry for this parent index */
+ foreach_ptr(PartitionIndexExtDepEntry, e, result)
+ {
+ if (e->parentIndexOid == parentIndexOid)
+ {
+ entry = e;
+ break;
+ }
+ }
+
+ if (entry != NULL)
+ {
+ /*
+ * We already have an entry for this parent index from a
+ * previous partition. Verify that the dependencies match
+ * exactly.
+ */
+ if (extDeps == NIL && entry->extensionOids != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot merge partitions with conflicting extension dependencies"),
+ errdetail("Partition index \"%s\" has no extension dependencies, but another partition's index does.",
+ get_rel_name(indexOid))));
+
+ if (extDeps != NIL && entry->extensionOids == NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot merge partitions with conflicting extension dependencies"),
+ errdetail("Partition index \"%s\" has extension dependencies, but another partition's index does not.",
+ get_rel_name(indexOid))));
+
+ foreach_oid(extOid, extDeps)
+ {
+ if (!list_member_oid(entry->extensionOids, extOid))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot merge partitions with conflicting extension dependencies"),
+ errdetail("Partition index \"%s\" depends on extension \"%s\", but another partition's index does not.",
+ get_rel_name(indexOid),
+ get_extension_name(extOid))));
+ }
+ }
+ else
+ {
+ /*
+ * First partition index we've seen for this parent. Create a
+ * new entry with its dependencies (may be NIL) - subsequent
+ * partitions will be checked against this.
+ */
+ entry = palloc(sizeof(PartitionIndexExtDepEntry));
+ entry->parentIndexOid = parentIndexOid;
+ entry->extensionOids = extDeps;
+ result = lappend(result, entry);
+ }
+ }
+
+ list_free(indexList);
+ table_close(partRel, NoLock);
+ }
+
+ 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.
+ */
+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;
+ ListCell *lc;
+
+ /* Skip if this is not a partition index */
+ if (!get_rel_relispartition(indexOid))
+ continue;
+
+ parentIdxOid = get_partition_parent(indexOid, true);
+ if (!OidIsValid(parentIdxOid))
+ continue;
+
+ /* Look for extension dependencies to apply */
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ if (entry->parentIndexOid == parentIdxOid)
+ {
+ ObjectAddress indexAddr;
+
+ 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)
+{
+ ListCell *lc;
+
+ foreach(lc, extDepState)
+ {
+ PartitionIndexExtDepEntry *entry = lfirst(lc);
+
+ list_free(entry->extensionOids);
+ pfree(entry);
+ }
+ list_free(extDepState);
+}
+
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
@@ -23009,6 +23225,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 +23315,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 +23399,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 +23702,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 +23741,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 +23830,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/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index 883110e25d9..541f88fdbb1 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1091,6 +1091,92 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4);
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6); -- Don't have a dependency
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5;
+-- Add only a single dependency to test that it fails when merge partition with
+-- different extension dependencies.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_3_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index t_merge_extdep_merged_i_idx because index t_merge_extdep_idx requires it
+HINT: You can drop index t_merge_extdep_idx instead.
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+ relname | extname
+-----------------------------+-----------
+ t_merge_extdep_merged_i_idx | test_ext3
+ t_merge_extdep_merged_x_idx | test_ext5
+(2 rows)
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_3_y_idx ON t_merge_extdep_3(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+-- The partition-only index is dropped with its partition
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged2%idx'
+ORDER BY relname;
+ relname
+------------------------------
+ t_merge_extdep_merged2_i_idx
+ t_merge_extdep_merged2_x_idx
+(2 rows)
+
+-- Should fail: Partitions to be merged have different extension dependencies.
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_4_i_idx" depends on extension "test_ext5", but another partition's index does not.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_merged2) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_merged2_i_idx" depends on extension "test_ext3", but another partition's index does not.
+-- Should fail: Partitions to be merged have different extension dependencies
+-- (t_merge_extdep_5 don't have dependencies)
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_5) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_5_i_idx" has no extension dependencies, but another partition's index does.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_5, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+ERROR: cannot merge partitions with conflicting extension dependencies
+DETAIL: Partition index "t_merge_extdep_4_i_idx" has extension dependencies, but another partition's index does not.
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
RESET search_path;
--
DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 43ca299648e..574119c11a7 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1589,6 +1589,47 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
(1 row)
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_1_3_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+ERROR: cannot drop index t_extdep_2_i_idx because index t_extdep_idx requires it
+HINT: You can drop index t_extdep_idx instead.
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | has_ext_dep
+------------------+-------------
+ t_extdep_1_i_idx | t
+ t_extdep_2_i_idx | t
+(2 rows)
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
RESET search_path;
--
DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
index a211fee2ad1..0f62129dcc1 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -784,6 +784,82 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after MERGE PARTITIONS.
+--
+CREATE EXTENSION IF NOT EXISTS test_ext3;
+CREATE EXTENSION IF NOT EXISTS test_ext5;
+
+CREATE TABLE t_merge_extdep (i int, x int, y int) PARTITION BY RANGE (i);
+CREATE TABLE t_merge_extdep_1 PARTITION OF t_merge_extdep FOR VALUES FROM (1) TO (2);
+CREATE TABLE t_merge_extdep_2 PARTITION OF t_merge_extdep FOR VALUES FROM (2) TO (3);
+CREATE TABLE t_merge_extdep_3 PARTITION OF t_merge_extdep FOR VALUES FROM (3) TO (4);
+CREATE TABLE t_merge_extdep_4 PARTITION OF t_merge_extdep FOR VALUES FROM (4) TO (5);
+CREATE TABLE t_merge_extdep_5 PARTITION OF t_merge_extdep FOR VALUES FROM (5) TO (6); -- Don't have a dependency
+CREATE INDEX t_merge_extdep_idx ON t_merge_extdep(i);
+CREATE INDEX t_merge_extdep_x ON t_merge_extdep(x);
+
+-- Add extension dependency for some partition indexes
+ALTER INDEX t_merge_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5;
+ALTER INDEX t_merge_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3;
+ALTER INDEX t_merge_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Add only a single dependency to test that it fails when merge partition with
+-- different extension dependencies.
+ALTER INDEX t_merge_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5;
+
+-- Should fail: dependencies exist
+DROP EXTENSION test_ext3;
+
+-- Merge partitions
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_1, t_merge_extdep_2) INTO t_merge_extdep_merged;
+
+-- Should still fail: dependencies should be preserved on the new partition's index
+DROP EXTENSION test_ext3;
+
+-- Verify that dependencies for all indexes exists in pg_depend
+SELECT relname, 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 ('t_merge_extdep_merged_i_idx', 't_merge_extdep_merged_x_idx')
+ AND e.extname IN ('test_ext3', 'test_ext5')
+ AND d.deptype = 'x'
+ORDER BY relname, extname;
+
+-- Create an index directly on a partition (without a parent partitioned index).
+-- Such indexes are not recreated on merge because they have no parent to map to.
+-- This test verifies that partition-only indexes don't cause issues during merge.
+CREATE INDEX t_merge_extdep_3_y_idx ON t_merge_extdep_3(y);
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged, t_merge_extdep_3) INTO t_merge_extdep_merged2;
+
+-- The partition-only index is dropped with its partition
+SELECT relname
+FROM pg_class
+WHERE relname LIKE 't_merge_extdep_merged2%idx'
+ORDER BY relname;
+
+-- Should fail: Partitions to be merged have different extension dependencies.
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_merged2, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_merged2) INTO t_merge_extdep_merged3;
+
+-- Should fail: Partitions to be merged have different extension dependencies
+-- (t_merge_extdep_5 don't have dependencies)
+-- Also test with different ordering to ensure correctness.
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_4, t_merge_extdep_5) INTO t_merge_extdep_merged3;
+ALTER TABLE t_merge_extdep MERGE PARTITIONS (t_merge_extdep_5, t_merge_extdep_4) INTO t_merge_extdep_merged3;
+
+-- Clean up
+DROP TABLE t_merge_extdep;
+DROP EXTENSION test_ext3;
+DROP EXTENSION test_ext5;
+
+
RESET search_path;
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index 44fcf208ac6..22563d3804c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1127,6 +1127,45 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
DROP TABLE t;
+--
+-- Test that extension dependencies on partition indexes are preserved
+-- after SPLIT PARTITION.
+--
+CREATE EXTENSION citext;
+
+CREATE TABLE t_extdep (i int) PARTITION BY RANGE (i);
+CREATE TABLE t_extdep_1_3 PARTITION OF t_extdep FOR VALUES FROM (1) TO (3);
+CREATE INDEX t_extdep_idx ON t_extdep (i);
+
+-- Add extension dependency on partition index
+ALTER INDEX t_extdep_1_3_i_idx DEPENDS ON EXTENSION citext;
+
+-- Should fail: dependency exists
+DROP EXTENSION citext;
+
+-- Split partition
+ALTER TABLE t_extdep SPLIT PARTITION t_extdep_1_3 INTO
+ (PARTITION t_extdep_1 FOR VALUES FROM (1) TO (2),
+ PARTITION t_extdep_2 FOR VALUES FROM (2) TO (3));
+
+-- Should still fail: dependencies should be preserved on all new partitions' indexes
+DROP EXTENSION citext;
+
+-- Verify the dependencies exist in pg_depend for both new partitions
+SELECT c.relname, COUNT(*) > 0 AS has_ext_dep
+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 ('t_extdep_1_i_idx', 't_extdep_2_i_idx')
+ AND e.extname = 'citext'
+ AND d.deptype = 'x'
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- Clean up
+DROP TABLE t_extdep;
+DROP EXTENSION citext;
+
RESET search_path;
--
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.53.0
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-21 15:57 Alexander Korotkov <[email protected]>
parent: Matheus Alcantara <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Alexander Korotkov @ 2026-04-21 15:57 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
Hi!
On Tue, Apr 21, 2026 at 4:20 PM Matheus Alcantara
<[email protected]> wrote:
> On Mon Apr 20, 2026 at 4:08 PM -03, Alexander Korotkov wrote:
> > On Thu, Apr 16, 2026 at 9:03 PM Matheus Alcantara
> > <[email protected]> wrote:
> >>
> >> On Tue Apr 14, 2026 at 6:05 AM -03, Dmitry Koval wrote:
> >> > Hi Matheus!
> >> >
> >> > Thank you for patch.
> >> > I agree that dependency should be automatically added for SPLIT
> >> > PARTITION. But I'm not sure about MERGE PARTITION ...
> >> > Might be it would be more correct to automatically add a dependency only
> >> > if all merged partitions have it?
> >>
> >> Hi,
> >>
> >> Thank you for taking a look on this!
> >>
> >> I agree with your suggestion. The attached patch implements the
> >> intersection behavior for MERGE PARTITIONS: extension dependencies are
> >> only preserved on the merged partition's index if all source partition
> >> indexes have that dependency.
> >>
> >> For example:
> >> MERGE(idx1(ext_a, ext_b), idx2(ext_a)) -> idx3(ext_a) -- only ext_a is common
> >> MERGE(idx1(ext_a), idx2()) -> idx3() -- no common deps
> >
> > This is not obvious for me. I would rather trigger an error if there
> > are different dependencies on merging partitions.
> >
>
> Yeah, I agree that this sounds a bit confusing, although this behavior
> is documented on the last patch version I think that raising an error is
> more simple and maybe is more obvious. The attached patch implement
> this.
I've spotted the following things in this patch.
1) The equality of dependencies is not fully checked. We only check
that for each new dependency, we have the same for previous partition,
but not vise versa.
2) The complexity of dependency checking is O(n^2).
3) Usage of citext and other extensions in src/test/regress where they
might be not available.
I've revised the patch.
1) collectPartitionIndexExtDeps() is rewritten(). Now it works in
three phases: collect, sort, compare. The comparison phase requires
strict equivalence of dependencies and doesn't depend on the order.
The complexity is now O(n * log(n)), which I think is acceptable.
2) PartitionIndexExtDepEntry struct now have indexOid. So, on
conflict error contains both partition index names.
3) Tests moved to
src/test/modules/test_extensions/sql/test_extdepend.sql where
test_ext3/test_ext5 extensions are available.
4) More tests for different scenarios.
Could you, please, review this changes?
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-21 19:23 Matheus Alcantara <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-21 19:23 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On 21/04/26 12:57, Alexander Korotkov wrote:
> I've spotted the following things in this patch.
> 1) The equality of dependencies is not fully checked. We only check
> that for each new dependency, we have the same for previous partition,
> but not vise versa.
> 2) The complexity of dependency checking is O(n^2).
> 3) Usage of citext and other extensions in src/test/regress where they
> might be not available.
>
Oops, I forgot to replace the citext extension on split partition tests.
> I've revised the patch.
> 1) collectPartitionIndexExtDeps() is rewritten(). Now it works in
> three phases: collect, sort, compare. The comparison phase requires
> strict equivalence of dependencies and doesn't depend on the order.
> The complexity is now O(n * log(n)), which I think is acceptable.
> 2) PartitionIndexExtDepEntry struct now have indexOid. So, on
> conflict error contains both partition index names.
> 3) Tests moved to
> src/test/modules/test_extensions/sql/test_extdepend.sql where
> test_ext3/test_ext5 extensions are available.
> 4) More tests for different scenarios.
>
> Could you, please, review this changes?
>
I think that you miss to include the patch?
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-21 19:35 Alexander Korotkov <[email protected]>
parent: Matheus Alcantara <[email protected]>
0 siblings, 2 replies; 21+ messages in thread
From: Alexander Korotkov @ 2026-04-21 19:35 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On Tue, Apr 21, 2026 at 10:23 PM Matheus Alcantara
<[email protected]> wrote:
> On 21/04/26 12:57, Alexander Korotkov wrote:
> > I've spotted the following things in this patch.
> > 1) The equality of dependencies is not fully checked. We only check
> > that for each new dependency, we have the same for previous partition,
> > but not vise versa.
> > 2) The complexity of dependency checking is O(n^2).
> > 3) Usage of citext and other extensions in src/test/regress where they
> > might be not available.
> >
>
> Oops, I forgot to replace the citext extension on split partition tests.
>
> > I've revised the patch.
> > 1) collectPartitionIndexExtDeps() is rewritten(). Now it works in
> > three phases: collect, sort, compare. The comparison phase requires
> > strict equivalence of dependencies and doesn't depend on the order.
> > The complexity is now O(n * log(n)), which I think is acceptable.
> > 2) PartitionIndexExtDepEntry struct now have indexOid. So, on
> > conflict error contains both partition index names.
> > 3) Tests moved to
> > src/test/modules/test_extensions/sql/test_extdepend.sql where
> > test_ext3/test_ext5 extensions are available.
> > 4) More tests for different scenarios.
> >
> > Could you, please, review this changes?
> >
>
> I think that you miss to include the patch?
Yep, here it is.
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v6-0001-Preserve-extension-dependencies-on-indexes-during.patch (28.1K, 2-v6-0001-Preserve-extension-dependencies-on-indexes-during.patch)
download | inline diff:
From f50671f727a039b12a5c219a0989b3b913e9dada Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Wed, 11 Mar 2026 11:39:56 -0300
Subject: [PATCH v6] 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 <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Reviewed-by: Alexander Korotkov <[email protected]>
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 | 127 ++++++++
.../test_extensions/sql/test_extdepend.sql | 105 +++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 533 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..1869549eeab 100644
--- a/src/test/modules/test_extensions/expected/test_extdepend.out
+++ b/src/test/modules/test_extensions/expected/test_extdepend.out
@@ -186,3 +186,130 @@ 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.
+-- Sanity check: the extension can't be dropped while dependencies exist.
+DROP EXTENSION test_ext3;
+ERROR: cannot drop index part_extdep_3_i_idx because index part_extdep_i_idx requires it
+HINT: You can drop index part_extdep_i_idx instead.
+-- Merge matching partitions: should succeed and preserve dependencies.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_1, part_extdep_2)
+ INTO part_extdep_merged;
+-- Dependencies must now sit on the new partition's indexes.
+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; merge must ignore such indexes (they disappear with the old
+-- partition).
+CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
+ INTO part_extdep_merged2;
+SELECT relname FROM pg_class
+WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
+ relname
+---------------------------
+ part_extdep_merged2_i_idx
+ part_extdep_merged2_x_idx
+(2 rows)
+
+-- 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..009d5354ad8 100644
--- a/src/test/modules/test_extensions/sql/test_extdepend.sql
+++ b/src/test/modules/test_extensions/sql/test_extdepend.sql
@@ -88,3 +88,108 @@ 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.
+
+-- Sanity check: the extension can't be dropped while dependencies exist.
+DROP EXTENSION test_ext3;
+
+-- Merge matching partitions: should succeed and preserve dependencies.
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_1, part_extdep_2)
+ INTO part_extdep_merged;
+
+-- Dependencies must now sit on the new partition's indexes.
+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; merge must ignore such indexes (they disappear with the old
+-- partition).
+CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
+ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
+ INTO part_extdep_merged2;
+SELECT relname FROM pg_class
+WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
+
+-- 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)
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 10:48 Kirill Reshke <[email protected]>
parent: Alexander Korotkov <[email protected]>
1 sibling, 2 replies; 21+ messages in thread
From: Kirill Reshke @ 2026-04-22 10:48 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On Wed, 22 Apr 2026 at 00:35, Alexander Korotkov <[email protected]> wrote:
>
> On Tue, Apr 21, 2026 at 10:23 PM Matheus Alcantara
> <[email protected]> wrote:
> > On 21/04/26 12:57, Alexander Korotkov wrote:
> > > I've spotted the following things in this patch.
> > > 1) The equality of dependencies is not fully checked. We only check
> > > that for each new dependency, we have the same for previous partition,
> > > but not vise versa.
> > > 2) The complexity of dependency checking is O(n^2).
> > > 3) Usage of citext and other extensions in src/test/regress where they
> > > might be not available.
> > >
> >
> > Oops, I forgot to replace the citext extension on split partition tests.
> >
> > > I've revised the patch.
> > > 1) collectPartitionIndexExtDeps() is rewritten(). Now it works in
> > > three phases: collect, sort, compare. The comparison phase requires
> > > strict equivalence of dependencies and doesn't depend on the order.
> > > The complexity is now O(n * log(n)), which I think is acceptable.
> > > 2) PartitionIndexExtDepEntry struct now have indexOid. So, on
> > > conflict error contains both partition index names.
> > > 3) Tests moved to
> > > src/test/modules/test_extensions/sql/test_extdepend.sql where
> > > test_ext3/test_ext5 extensions are available.
> > > 4) More tests for different scenarios.
> > >
> > > Could you, please, review this changes?
> > >
> >
> > I think that you miss to include the patch?
>
> Yep, here it is.
>
> ------
> Regards,
> Alexander Korotkov
> Supabase
Hi!
So, we only transfer dependencies on partitioned indexes when
SPLIT/MERGE partitions, not tables themselves (this is what this
thread started from).
I think this is correct.
Some minor comments:
> +-- Sanity check: the extension can't be dropped while dependencies exist.
>+DROP EXTENSION test_ext3;
This exercises something that already works on HEAD (note this is DROP
before first MERGE partition call ). Do we really need this?
>
> +-- An index created directly on a partition has no parent in the partitioned
> +-- index tree; merge must ignore such indexes (they disappear with the old
> +-- partition).
> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
> + INTO part_extdep_merged2;
> +SELECT relname FROM pg_class
> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
Looks like this test is also redundant? This does not test new DEPENDS ON logic.
Otherwise LGTM
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 10:53 Matheus Alcantara <[email protected]>
parent: Alexander Korotkov <[email protected]>
1 sibling, 0 replies; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-22 10:53 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On Tue Apr 21, 2026 at 4:35 PM -03, Alexander Korotkov wrote:
>> > I've revised the patch.
>> > 1) collectPartitionIndexExtDeps() is rewritten(). Now it works in
>> > three phases: collect, sort, compare. The comparison phase requires
>> > strict equivalence of dependencies and doesn't depend on the order.
>> > The complexity is now O(n * log(n)), which I think is acceptable.
>> > 2) PartitionIndexExtDepEntry struct now have indexOid. So, on
>> > conflict error contains both partition index names.
>> > 3) Tests moved to
>> > src/test/modules/test_extensions/sql/test_extdepend.sql where
>> > test_ext3/test_ext5 extensions are available.
>> > 4) More tests for different scenarios.
>> >
>> > Could you, please, review this changes?
>> >
>>
>> I think that you miss to include the patch?
>
> Yep, here it is.
>
Thanks for the patch. It looks good to me, and I confirm that it fix the
issues that I miss on the previous version.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 10:58 Matheus Alcantara <[email protected]>
parent: Kirill Reshke <[email protected]>
1 sibling, 1 reply; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-22 10:58 UTC (permalink / raw)
To: Kirill Reshke <[email protected]>; Alexander Korotkov <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On Wed Apr 22, 2026 at 7:48 AM -03, Kirill Reshke wrote:
>> +-- An index created directly on a partition has no parent in the partitioned
>> +-- index tree; merge must ignore such indexes (they disappear with the old
>> +-- partition).
>> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
>> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
>> + INTO part_extdep_merged2;
>> +SELECT relname FROM pg_class
>> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
>
> Looks like this test is also redundant? This does not test new DEPENDS ON logic.
>
I think that this test is useful to ensure that we correctly skip such
indexes created directly on a specific partition. Perhaps we can include
an ALTER INDEX ... DEPENDS ON for this specific index to make it more
consistent with the other tests?
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 10:59 Kirill Reshke <[email protected]>
parent: Matheus Alcantara <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Kirill Reshke @ 2026-04-22 10:59 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On Wed, 22 Apr 2026 at 15:58, Matheus Alcantara
<[email protected]> wrote:
>
> On Wed Apr 22, 2026 at 7:48 AM -03, Kirill Reshke wrote:
> >> +-- An index created directly on a partition has no parent in the partitioned
> >> +-- index tree; merge must ignore such indexes (they disappear with the old
> >> +-- partition).
> >> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
> >> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
> >> + INTO part_extdep_merged2;
> >> +SELECT relname FROM pg_class
> >> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
> >
> > Looks like this test is also redundant? This does not test new DEPENDS ON logic.
> >
>
> I think that this test is useful to ensure that we correctly skip such
> indexes created directly on a specific partition. Perhaps we can include
> an ALTER INDEX ... DEPENDS ON for this specific index to make it more
> consistent with the other tests?
>
> --
> Matheus Alcantara
> EDB: https://www.enterprisedb.com
This test is maybe useful, but this is unrelated to what this thread &
fix is about, for my taste.
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 11:05 Matheus Alcantara <[email protected]>
parent: Kirill Reshke <[email protected]>
0 siblings, 0 replies; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-22 11:05 UTC (permalink / raw)
To: Kirill Reshke <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On Wed Apr 22, 2026 at 7:59 AM -03, Kirill Reshke wrote:
> On Wed, 22 Apr 2026 at 15:58, Matheus Alcantara
> <[email protected]> wrote:
>>
>> On Wed Apr 22, 2026 at 7:48 AM -03, Kirill Reshke wrote:
>> >> +-- An index created directly on a partition has no parent in the partitioned
>> >> +-- index tree; merge must ignore such indexes (they disappear with the old
>> >> +-- partition).
>> >> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
>> >> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
>> >> + INTO part_extdep_merged2;
>> >> +SELECT relname FROM pg_class
>> >> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
>> >
>> > Looks like this test is also redundant? This does not test new DEPENDS ON logic.
>> >
>>
>> I think that this test is useful to ensure that we correctly skip such
>> indexes created directly on a specific partition. Perhaps we can include
>> an ALTER INDEX ... DEPENDS ON for this specific index to make it more
>> consistent with the other tests?
>
> This test is maybe useful, but this is unrelated to what this thread &
> fix is about, for my taste.
>
On collectPartitionIndexExtDeps() we have:
if (!get_rel_relispartition(indexOid))
continue;
parentIndexOid = get_partition_parent(indexOid, true);
if (!OidIsValid(parentIndexOid))
continue;
I think that this test ensure that get_rel_relispartition() check is
called, otherwise we will call get_partition_parent() with an index that
don't have parent which will fail with an elog(ERROR).
It can be unrelated but I think that it ensure correctness for this fix,
or I'm missing something here?
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 11:24 Alexander Korotkov <[email protected]>
parent: Kirill Reshke <[email protected]>
1 sibling, 2 replies; 21+ messages in thread
From: Alexander Korotkov @ 2026-04-22 11:24 UTC (permalink / raw)
To: Kirill Reshke <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On Wed, Apr 22, 2026 at 1:48 PM Kirill Reshke <[email protected]> wrote:
> Some minor comments:
>
> > +-- Sanity check: the extension can't be dropped while dependencies exist.
> >+DROP EXTENSION test_ext3;
>
> This exercises something that already works on HEAD (note this is DROP
> before first MERGE partition call ). Do we really need this?
I've removed this.
> > +-- An index created directly on a partition has no parent in the partitioned
> > +-- index tree; merge must ignore such indexes (they disappear with the old
> > +-- partition).
> > +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
> > +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
> > + INTO part_extdep_merged2;
> > +SELECT relname FROM pg_class
> > +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
>
> Looks like this test is also redundant? This does not test new DEPENDS ON logic.
I've added the dependency on this index to check index disappears with
its dependency. I think this would make this test more relevant.
Kirill, Matheus, are you ok with these change?
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v7-0001-Preserve-extension-dependencies-on-indexes-during.patch (28.2K, 2-v7-0001-Preserve-extension-dependencies-on-indexes-during.patch)
download | inline diff:
From 37778708a48087f70496fcd649c4a59d873e0644 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
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 <[email protected]>
Reported-by: Kirill Reshke
Reviewed-by: Dmitry Koval <[email protected]>
Reviewed-by: Alexander Korotkov <[email protected]>
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)
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 11:26 Kirill Reshke <[email protected]>
parent: Alexander Korotkov <[email protected]>
1 sibling, 0 replies; 21+ messages in thread
From: Kirill Reshke @ 2026-04-22 11:26 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On Wed, 22 Apr 2026 at 16:24, Alexander Korotkov <[email protected]> wrote:
>
> On Wed, Apr 22, 2026 at 1:48 PM Kirill Reshke <[email protected]> wrote:
> > Some minor comments:
> >
> > > +-- Sanity check: the extension can't be dropped while dependencies exist.
> > >+DROP EXTENSION test_ext3;
> >
> > This exercises something that already works on HEAD (note this is DROP
> > before first MERGE partition call ). Do we really need this?
>
> I've removed this.
>
> > > +-- An index created directly on a partition has no parent in the partitioned
> > > +-- index tree; merge must ignore such indexes (they disappear with the old
> > > +-- partition).
> > > +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
> > > +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
> > > + INTO part_extdep_merged2;
> > > +SELECT relname FROM pg_class
> > > +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
> >
> > Looks like this test is also redundant? This does not test new DEPENDS ON logic.
>
> I've added the dependency on this index to check index disappears with
> its dependency. I think this would make this test more relevant.
>
> Kirill, Matheus, are you ok with these change?
>
> ------
> Regards,
> Alexander Korotkov
> Supabase
v7 WFM
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 11:30 Matheus Alcantara <[email protected]>
parent: Alexander Korotkov <[email protected]>
1 sibling, 1 reply; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-22 11:30 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; Kirill Reshke <[email protected]>; +Cc: Dmitry Koval <[email protected]>; pgsql-hackers
On 22/04/26 08:24, Alexander Korotkov wrote:
>>> +-- An index created directly on a partition has no parent in the partitioned
>>> +-- index tree; merge must ignore such indexes (they disappear with the old
>>> +-- partition).
>>> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
>>> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
>>> + INTO part_extdep_merged2;
>>> +SELECT relname FROM pg_class
>>> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
>>
>> Looks like this test is also redundant? This does not test new DEPENDS ON logic.
>
> I've added the dependency on this index to check index disappears with
> its dependency. I think this would make this test more relevant.
>
> Kirill, Matheus, are you ok with these change?
>
It works for me.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 11:34 Alexander Korotkov <[email protected]>
parent: Matheus Alcantara <[email protected]>
0 siblings, 1 reply; 21+ messages in thread
From: Alexander Korotkov @ 2026-04-22 11:34 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; +Cc: Kirill Reshke <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On Wed, Apr 22, 2026 at 2:30 PM Matheus Alcantara
<[email protected]> wrote:
> On 22/04/26 08:24, Alexander Korotkov wrote:
> >>> +-- An index created directly on a partition has no parent in the partitioned
> >>> +-- index tree; merge must ignore such indexes (they disappear with the old
> >>> +-- partition).
> >>> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
> >>> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
> >>> + INTO part_extdep_merged2;
> >>> +SELECT relname FROM pg_class
> >>> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
> >>
> >> Looks like this test is also redundant? This does not test new DEPENDS ON logic.
> >
> > I've added the dependency on this index to check index disappears with
> > its dependency. I think this would make this test more relevant.
> >
> > Kirill, Matheus, are you ok with these change?
> >
> It works for me.
Thank you, pushed!
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 21+ messages in thread
* Re: MERGE PARTITIONS and DEPENDS ON EXTENSION.
@ 2026-04-22 11:42 Matheus Alcantara <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 0 replies; 21+ messages in thread
From: Matheus Alcantara @ 2026-04-22 11:42 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Kirill Reshke <[email protected]>; Dmitry Koval <[email protected]>; pgsql-hackers
On 22/04/26 08:34, Alexander Korotkov wrote:
> On Wed, Apr 22, 2026 at 2:30 PM Matheus Alcantara
> <[email protected]> wrote:
>> On 22/04/26 08:24, Alexander Korotkov wrote:
>>>>> +-- An index created directly on a partition has no parent in the partitioned
>>>>> +-- index tree; merge must ignore such indexes (they disappear with the old
>>>>> +-- partition).
>>>>> +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x);
>>>>> +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3)
>>>>> + INTO part_extdep_merged2;
>>>>> +SELECT relname FROM pg_class
>>>>> +WHERE relname LIKE 'part_extdep_merged2%idx' ORDER BY relname;
>>>>
>>>> Looks like this test is also redundant? This does not test new DEPENDS ON logic.
>>>
>>> I've added the dependency on this index to check index disappears with
>>> its dependency. I think this would make this test more relevant.
>>>
>>> Kirill, Matheus, are you ok with these change?
>>>
>> It works for me.
>
> Thank you, pushed!
>
Thank you!
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 21+ messages in thread
end of thread, other threads:[~2026-04-22 11:42 UTC | newest]
Thread overview: 21+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-03-10 17:53 MERGE PARTITIONS and DEPENDS ON EXTENSION. Kirill Reshke <[email protected]>
2026-04-07 14:14 ` Matheus Alcantara <[email protected]>
2026-04-14 09:05 ` Dmitry Koval <[email protected]>
2026-04-16 18:03 ` Matheus Alcantara <[email protected]>
2026-04-20 00:04 ` Dmitry Koval <[email protected]>
2026-04-20 15:23 ` Matheus Alcantara <[email protected]>
2026-04-20 19:08 ` Alexander Korotkov <[email protected]>
2026-04-21 13:20 ` Matheus Alcantara <[email protected]>
2026-04-21 15:57 ` Alexander Korotkov <[email protected]>
2026-04-21 19:23 ` Matheus Alcantara <[email protected]>
2026-04-21 19:35 ` Alexander Korotkov <[email protected]>
2026-04-22 10:48 ` Kirill Reshke <[email protected]>
2026-04-22 10:58 ` Matheus Alcantara <[email protected]>
2026-04-22 10:59 ` Kirill Reshke <[email protected]>
2026-04-22 11:05 ` Matheus Alcantara <[email protected]>
2026-04-22 11:24 ` Alexander Korotkov <[email protected]>
2026-04-22 11:26 ` Kirill Reshke <[email protected]>
2026-04-22 11:30 ` Matheus Alcantara <[email protected]>
2026-04-22 11:34 ` Alexander Korotkov <[email protected]>
2026-04-22 11:42 ` Matheus Alcantara <[email protected]>
2026-04-22 10:53 ` Matheus Alcantara <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox