From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Fri, 17 Jul 2026 12:11:36 +0530
Subject: [PATCH v1] Cache a partition index's immediate parent OID in the
 relcache

get_partition_ancestors() scans pg_inherits once per hierarchy level on
every call.  In the INSERT ... ON CONFLICT arbiter-mapping loop in
ExecInitPartitionInfo() this runs once per leaf index; the existing XXX
comment there flagged it as slow.

Add get_partition_index_ancestors(Relation indexRel), which caches the
immediate partition-parent OID of an already-open, locked partition
index on its relcache entry (rd_partparent, populated lazily) and walks
higher levels with the existing catalog-scanning helper.  Only the leaf
index's parent is cached, using the relation the caller already holds a
lock on, so no unlocked relcache opens are introduced.

Supported changes to index ancestry already invalidate the affected
index relcache entry.  ATTACH and DETACH update the child index's
pg_class row, while index_concurrently_swap() updates both pg_class rows
as it transfers the pg_inherits link; creation and drop replace the
relation altogether.  Reset the cached parent during both full relcache
builds and RelationReloadIndexInfo(), the minimal reload used for open
indexes.

Add a regression that warms the cache, moves a partition between two
partitioned tables, and verifies the reverse invalidation across a
savepoint rollback.
---
 src/backend/catalog/partition.c               | 76 +++++++++++++++++++
 src/backend/executor/execPartition.c          | 11 ++-
 src/backend/utils/cache/relcache.c            |  6 ++
 src/include/catalog/partition.h               |  1 +
 src/include/utils/rel.h                       |  5 ++
 src/test/regress/expected/insert_conflict.out | 28 +++++++
 src/test/regress/sql/insert_conflict.sql      | 30 ++++++++
 7 files changed, 151 insertions(+), 6 deletions(-)

diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 28f3cade6ff..166a544b2cf 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -27,6 +27,7 @@
 #include "optimizer/optimizer.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
 #include "utils/partcache.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
@@ -35,6 +36,7 @@ static Oid	get_partition_parent_worker(Relation inhRel, Oid relid,
 										bool *detach_pending);
 static void get_partition_ancestors_worker(Relation inhRel, Oid relid,
 										   List **ancestors);
+static Oid	RelationGetPartitionParent(Relation rel);
 
 /*
  * get_partition_parent
@@ -167,6 +169,80 @@ get_partition_ancestors_worker(Relation inhRel, Oid relid, List **ancestors)
 	get_partition_ancestors_worker(inhRel, parentOid, ancestors);
 }
 
+/*
+ * get_partition_index_ancestors
+ *		Obtain ancestors using the immediate-parent OID cached on 'indexRel'.
+ *
+ * The caller must already have opened and locked 'indexRel'.  Higher levels
+ * use the regular catalog walker, because opening their relcache entries by
+ * OID without holding relation locks would be unsafe.
+ */
+List *
+get_partition_index_ancestors(Relation indexRel)
+{
+	List	   *result;
+	Oid			parentOid;
+
+	Assert(indexRel->rd_rel->relkind == RELKIND_INDEX);
+
+	parentOid = RelationGetPartitionParent(indexRel);
+	if (!OidIsValid(parentOid))
+		return NIL;
+
+	result = list_make1_oid(parentOid);
+	if (get_rel_relispartition(parentOid))
+		result = list_concat(result, get_partition_ancestors(parentOid));
+
+	return result;
+}
+
+/*
+ * RelationGetPartitionParent
+ *		Return the OID of the immediate partition parent of 'rel', caching it
+ *		on the relcache entry.
+ *
+ * Returns InvalidOid when 'rel' is not a partition or has no parent.
+ *
+ * The cached value is reset by both full relcache rebuilds and the minimal
+ * reload used for open indexes.  Changes to an index partition's pg_inherits
+ * link either create or drop the index, or update its pg_class row and cause
+ * a relcache invalidation.
+ */
+static Oid
+RelationGetPartitionParent(Relation rel)
+{
+	Relation	inhRel;
+	Oid			parentOid;
+	bool		detach_pending;
+
+	if (rel->rd_partparentvalid)
+		return rel->rd_partparent;
+
+	if (!rel->rd_rel->relispartition)
+	{
+		rel->rd_partparent = InvalidOid;
+		rel->rd_partparentvalid = true;
+		return InvalidOid;
+	}
+
+	inhRel = table_open(InheritsRelationId, AccessShareLock);
+	parentOid = get_partition_parent_worker(inhRel, RelationGetRelid(rel),
+											&detach_pending);
+	table_close(inhRel, AccessShareLock);
+
+	/*
+	 * Only leaf partition indexes reach here, and an index's pg_inherits row
+	 * is never marked detach-pending: DETACH PARTITION CONCURRENTLY sets that
+	 * flag on the table partition's row, not on its indexes (see
+	 * MarkInheritDetached).  So the immediate parent link is always live.
+	 */
+	Assert(!detach_pending);
+
+	rel->rd_partparent = parentOid;
+	rel->rd_partparentvalid = true;
+	return parentOid;
+}
+
 /*
  * index_get_partition
  *		Return the OID of index of the given partition that is a child
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 33ec5bfde4c..196ad46d77d 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -786,14 +786,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 				 * This will have the effect of also treating that second
 				 * index as arbiter.
 				 *
-				 * XXX get_partition_ancestors scans pg_inherits, which is not
-				 * only slow, but also means the catalog snapshot can get
-				 * invalidated each time through the loop (cf.
-				 * GetNonHistoricCatalogSnapshot).  Consider a syscache or
-				 * some other way to cache?
+				 * Use the already-open index relation so its immediate parent
+				 * can be cached on the relcache entry, avoiding repeated
+				 * pg_inherits scans in this loop.
 				 */
 				indexoid = RelationGetRelid(leaf_part_rri->ri_IndexRelationDescs[listidx]);
-				ancestors = get_partition_ancestors(indexoid);
+				ancestors =
+					get_partition_index_ancestors(leaf_part_rri->ri_IndexRelationDescs[listidx]);
 				INJECTION_POINT("exec-init-partition-after-get-partition-ancestors", NULL);
 
 				if (ancestors != NIL &&
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 19c4ff6e75e..6713734f320 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1218,6 +1218,8 @@ retry:
 	relation->rd_partcheck = NIL;
 	relation->rd_partcheckvalid = false;
 	relation->rd_partcheckcxt = NULL;
+	relation->rd_partparent = InvalidOid;
+	relation->rd_partparentvalid = false;
 
 	/*
 	 * initialize access method information
@@ -2318,6 +2320,8 @@ RelationReloadIndexInfo(Relation relation)
 			 RelationGetRelid(relation));
 	relp = (Form_pg_class) GETSTRUCT(pg_class_tuple);
 	memcpy(relation->rd_rel, relp, CLASS_TUPLE_SIZE);
+	relation->rd_partparent = InvalidOid;
+	relation->rd_partparentvalid = false;
 	/* Reload reloptions in case they changed */
 	if (relation->rd_options)
 		pfree(relation->rd_options);
@@ -6500,6 +6504,8 @@ load_relcache_init_file(bool shared)
 		rel->rd_partcheck = NIL;
 		rel->rd_partcheckvalid = false;
 		rel->rd_partcheckcxt = NULL;
+		rel->rd_partparent = InvalidOid;
+		rel->rd_partparentvalid = false;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h
index a93cf081dd2..6b7064b8ecf 100644
--- a/src/include/catalog/partition.h
+++ b/src/include/catalog/partition.h
@@ -21,6 +21,7 @@
 
 extern Oid	get_partition_parent(Oid relid, bool even_if_detached);
 extern List *get_partition_ancestors(Oid relid);
+extern List *get_partition_index_ancestors(Relation indexRel);
 extern Oid	index_get_partition(Relation partition, Oid indexId);
 extern List *map_partition_varattnos(List *expr, int fromrel_varno,
 									 Relation to_rel, Relation from_rel);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..0a7a5a5927b 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -148,6 +148,11 @@ typedef struct RelationData
 	bool		rd_partcheckvalid;	/* true if list has been computed */
 	MemoryContext rd_partcheckcxt;	/* private cxt for rd_partcheck, if any */
 
+	/* data managed by RelationGetPartitionParent: */
+	Oid			rd_partparent;	/* OID of immediate partition parent, or
+								 * InvalidOid; valid only if rd_partparentvalid */
+	bool		rd_partparentvalid; /* true if rd_partparent has been computed */
+
 	/* data managed by RelationGetIndexList: */
 	List	   *rd_indexlist;	/* list of OIDs of indexes on relation */
 	Oid			rd_pkindex;		/* OID of (deferrable?) primary key, if any */
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 34e2e7ee355..fdabb23ca13 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -1042,6 +1042,34 @@ insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do selec
 (1 row)
 
 drop table parted_conflict_test;
+-- check that a cached index parent is invalidated when its partition is
+-- detached and attached to another partitioned table
+create table reparent_conflict_a (i int primary key) partition by range (i);
+create table reparent_conflict_b (i int primary key) partition by range (i);
+create table reparent_conflict_child partition of reparent_conflict_a
+  for values from (0) to (10);
+insert into reparent_conflict_a values (1)
+  on conflict (i) do update set i = excluded.i;
+begin;
+alter table reparent_conflict_a detach partition reparent_conflict_child;
+alter table reparent_conflict_b attach partition reparent_conflict_child
+  for values from (0) to (10);
+insert into reparent_conflict_b values (1)
+  on conflict (i) do update set i = excluded.i;
+commit;
+-- check the reverse invalidation when reparenting is rolled back
+begin;
+savepoint reparent_conflict_savepoint;
+alter table reparent_conflict_b detach partition reparent_conflict_child;
+alter table reparent_conflict_a attach partition reparent_conflict_child
+  for values from (0) to (10);
+insert into reparent_conflict_a values (1)
+  on conflict (i) do update set i = excluded.i;
+rollback to reparent_conflict_savepoint;
+insert into reparent_conflict_b values (1)
+  on conflict (i) do update set i = excluded.i;
+commit;
+drop table reparent_conflict_a, reparent_conflict_b;
 -- test behavior of inserting a conflicting tuple into an intermediate
 -- partitioning level
 create table parted_conflict (a int primary key, b text) partition by range (a);
diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql
index a5a84d1d4b8..03a5198b1ac 100644
--- a/src/test/regress/sql/insert_conflict.sql
+++ b/src/test/regress/sql/insert_conflict.sql
@@ -596,6 +596,36 @@ insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do selec
 
 drop table parted_conflict_test;
 
+-- check that a cached index parent is invalidated when its partition is
+-- detached and attached to another partitioned table
+create table reparent_conflict_a (i int primary key) partition by range (i);
+create table reparent_conflict_b (i int primary key) partition by range (i);
+create table reparent_conflict_child partition of reparent_conflict_a
+  for values from (0) to (10);
+insert into reparent_conflict_a values (1)
+  on conflict (i) do update set i = excluded.i;
+begin;
+alter table reparent_conflict_a detach partition reparent_conflict_child;
+alter table reparent_conflict_b attach partition reparent_conflict_child
+  for values from (0) to (10);
+insert into reparent_conflict_b values (1)
+  on conflict (i) do update set i = excluded.i;
+commit;
+
+-- check the reverse invalidation when reparenting is rolled back
+begin;
+savepoint reparent_conflict_savepoint;
+alter table reparent_conflict_b detach partition reparent_conflict_child;
+alter table reparent_conflict_a attach partition reparent_conflict_child
+  for values from (0) to (10);
+insert into reparent_conflict_a values (1)
+  on conflict (i) do update set i = excluded.i;
+rollback to reparent_conflict_savepoint;
+insert into reparent_conflict_b values (1)
+  on conflict (i) do update set i = excluded.i;
+commit;
+drop table reparent_conflict_a, reparent_conflict_b;
+
 -- test behavior of inserting a conflicting tuple into an intermediate
 -- partitioning level
 create table parted_conflict (a int primary key, b text) partition by range (a);
-- 
2.43.0

