From d75e0f51639f5d5a275f2066ff1ac9b01396835c Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Sat, 20 Jun 2026 09:37:58 +0100
Subject: [PATCH v7 10/10] Add pg_temp_index global temporary catalog table.

This just has indexrelid and indisvalid columns, allowing the valid
state of an index to session-specific. This is needed when one session
defines an index on a global temporary table, while another session is
using the table, and has already populated it. In all other cases,
pg_index.indexrelid and pg_temp_index.indexrelid are expected to be
the same.
---
 contrib/tcn/tcn.c                           |   7 +-
 src/backend/access/transam/xact.c           |   9 +-
 src/backend/catalog/global_temp.c           |  39 +-
 src/backend/catalog/index.c                 |  48 +-
 src/backend/catalog/pg_temp_class.c         | 637 +++++++++++++++++---
 src/backend/commands/indexcmds.c            |  41 +-
 src/backend/commands/repack.c               |   3 +-
 src/backend/commands/tablecmds.c            |  39 +-
 src/backend/utils/cache/lsyscache.c         |   4 +-
 src/backend/utils/cache/relcache.c          |  44 +-
 src/bin/psql/describe.c                     |  80 ++-
 src/include/catalog/Makefile                |   1 +
 src/include/catalog/meson.build             |   1 +
 src/include/catalog/pg_temp_class.h         |   9 +-
 src/include/catalog/pg_temp_index.h         |  66 ++
 src/test/isolation/expected/global-temp.out |  81 +++
 src/test/isolation/specs/global-temp.spec   |   2 +
 src/test/regress/expected/global_temp.out   | 107 +++-
 src/test/regress/expected/oidjoins.out      |   1 +
 src/test/regress/sql/global_temp.sql        |  34 ++
 src/tools/pgindent/typedefs.list            |   2 +
 21 files changed, 1084 insertions(+), 171 deletions(-)
 create mode 100644 src/include/catalog/pg_temp_index.h

diff --git a/contrib/tcn/tcn.c b/contrib/tcn/tcn.c
index 6b4bc7960d9..3201f438c4b 100644
--- a/contrib/tcn/tcn.c
+++ b/contrib/tcn/tcn.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "catalog/pg_temp_class.h"
 #include "commands/async.h"
 #include "commands/trigger.h"
 #include "executor/spi.h"
@@ -135,7 +136,7 @@ triggered_change_notification(PG_FUNCTION_ARGS)
 		HeapTuple	indexTuple;
 		Form_pg_index index;
 
-		indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+		indexTuple = GetEffectivePgIndexTuple(indexoid);
 		if (!HeapTupleIsValid(indexTuple))	/* should not happen */
 			elog(ERROR, "cache lookup failed for index %u", indexoid);
 		index = (Form_pg_index) GETSTRUCT(indexTuple);
@@ -167,10 +168,10 @@ triggered_change_notification(PG_FUNCTION_ARGS)
 
 				Async_Notify(channel, payload.data);
 			}
-			ReleaseSysCache(indexTuple);
+			heap_freetuple(indexTuple);
 			break;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 	}
 
 	list_free(indexoidlist);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7c631de9393..8fbbb425e2c 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -37,6 +37,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_enum.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
 #include "commands/tablecmds.h"
@@ -1149,7 +1150,7 @@ CommandCounterIncrement(void)
 					(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 					 errmsg("cannot start commands during a parallel operation")));
 
-		/* Flush out any pending inserts to pg_temp_class */
+		/* Flush out any pending pg_temp_class and pg_temp_index inserts */
 		PreCCI_PgTempClass();
 
 		currentCommandId += 1;
@@ -2364,7 +2365,7 @@ CommitTransaction(void)
 	 */
 	PreCommit_on_commit_actions();
 
-	/* Flush out any pending inserts to pg_temp_class */
+	/* Flush out any pending pg_temp_class and pg_temp_index inserts */
 	PreCommit_PgTempClass();
 
 	/*
@@ -2640,7 +2641,7 @@ PrepareTransaction(void)
 	 */
 	PreCommit_on_commit_actions();
 
-	/* Flush out any pending inserts to pg_temp_class */
+	/* Flush out any pending pg_temp_class and pg_temp_index inserts */
 	PreCommit_PgTempClass();
 
 	/*
@@ -5202,7 +5203,7 @@ CommitSubTransaction(void)
 	CallSubXactCallbacks(SUBXACT_EVENT_PRE_COMMIT_SUB, s->subTransactionId,
 						 s->parent->subTransactionId);
 
-	/* Flush out any pending inserts to pg_temp_class */
+	/* Flush out any pending pg_temp_class and pg_temp_index inserts */
 	PreSubCommit_PgTempClass();
 
 	/*
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 7a7e77a0d8e..1fbd3bfad9f 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -122,6 +122,7 @@ static bool eoxact_storage_list_overflowed = false;
 typedef struct GtrUsageEntry
 {
 	Oid			relid;			/* lookup key: OID of relation in use */
+	char		relkind;		/* relkind of the relation */
 	SubTransactionId started_subid; /* usage started in current xact */
 	SubTransactionId stopped_subid; /* usage ended with another subid set */
 } GtrUsageEntry;
@@ -519,7 +520,7 @@ gtr_init_usage_tables(void)
  *	Note: This is intentionally idempotent.
  */
 static void
-gtr_record_usage(Oid relid)
+gtr_record_usage(Oid relid, char relkind)
 {
 	GtrUsageEntry *local_entry;
 	GtrSharedUsageKey key;
@@ -534,6 +535,9 @@ gtr_record_usage(Oid relid)
 	if (found)
 		return;					/* already recorded, nothing to do */
 
+	/* Remember the relation's relkind */
+	local_entry->relkind = relkind;
+
 	/* Record the usage as starting in the current subtransaction */
 	local_entry->started_subid = GetCurrentSubTransactionId();
 	local_entry->stopped_subid = InvalidSubTransactionId;
@@ -972,14 +976,33 @@ GlobalTempRelationCreated(Relation relation)
 {
 	/*
 	 * If this is the first time we've used this relation in this session,
-	 * insert a pg_temp_class tuple for it, and update the usage hash tables.
+	 * insert pg_temp_class and pg_temp_index tuples for it, and update the
+	 * usage hash tables.
 	 */
 	if (gtr_local_usage == NULL ||
 		hash_search(gtr_local_usage,
 					&relation->rd_id, HASH_FIND, NULL) == NULL)
 	{
 		InsertPgTempClassTuple(relation);
-		gtr_record_usage(relation->rd_id);
+
+		if (relation->rd_rel->relkind == RELKIND_INDEX ||
+			relation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
+		{
+			/*
+			 * When creating a new index locally, relation->rd_index will be
+			 * NULL here.  Mark it as valid for now --- UpdateIndexRelation()
+			 * will update it later, if it's not actually valid (e.g., CREATE
+			 * INDEX ... ON ONLY ...).  Otherwise, for an index created in
+			 * another session, relation->rd_index->indisvalid will accurately
+			 * reflect whether or not the index needs to be marked invalid
+			 * locally (if our instance of the index's table is not empty).
+			 */
+			InsertPgTempIndexTuple(relation->rd_id,
+								   relation->rd_index == NULL ||
+								   relation->rd_index->indisvalid);
+		}
+
+		gtr_record_usage(relation->rd_id, relation->rd_rel->relkind);
 	}
 }
 
@@ -1010,8 +1033,12 @@ GlobalTempRelationDropped(Oid relid)
 		/* Flag the usage entry for eoxact cleanup */
 		EOXactUsageListAdd(relid);
 
-		/* Delete it's pg_temp_class tuple */
+		/* Delete it's pg_temp_class and pg_temp_index tuples */
 		DeletePgTempClassTuple(relid);
+
+		if (entry->relkind == RELKIND_INDEX ||
+			entry->relkind == RELKIND_PARTITIONED_INDEX)
+			DeletePgTempIndexTuple(relid);
 	}
 
 	/* Forget any ON COMMIT action for the relation */
@@ -1141,7 +1168,7 @@ AtEOXact_GlobalTempRelation(bool isCommit)
 		}
 	}
 
-	/* Perform any pg_temp_class processing */
+	/* Perform any pg_temp_class and pg_temp_index processing */
 	AtEOXact_PgTempClass(isCommit);
 
 	/* Now we're out of the transaction and can clear the lists */
@@ -1215,7 +1242,7 @@ AtEOSubXact_GlobalTempRelation(bool isCommit, SubTransactionId mySubid,
 		}
 	}
 
-	/* Perform any pg_temp_class processing */
+	/* Perform any pg_temp_class and pg_temp_index processing */
 	AtEOSubXact_PgTempClass(isCommit, mySubid, parentSubid);
 
 	/* Don't reset the lists; we still need more cleanup later */
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 943de8a4fda..99123bde736 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -50,6 +50,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -121,7 +122,8 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 								bool isexclusion,
 								bool immediate,
 								bool isvalid,
-								bool isready);
+								bool isready,
+								char relpersistence);
 static void index_update_stats(Relation rel,
 							   bool isreindex,
 							   bool hasindex,
@@ -574,7 +576,8 @@ UpdateIndexRelation(Oid indexoid,
 					bool isexclusion,
 					bool immediate,
 					bool isvalid,
-					bool isready)
+					bool isready,
+					char relpersistence)
 {
 	int2vector *indkey;
 	oidvector  *indcollation;
@@ -675,6 +678,23 @@ UpdateIndexRelation(Oid indexoid,
 	 */
 	table_close(pg_index, RowExclusiveLock);
 	heap_freetuple(tuple);
+
+	/*
+	 * For an index on a global temporary table, GlobalTempRelationCreated()
+	 * will have inserted a pg_temp_index tuple with indisvalid = true.  If
+	 * the index is actually not valid, fix that now.
+	 */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP && !isvalid)
+	{
+		tuple = GetPgTempIndexTuple(indexoid);
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for global temp index %u", indexoid);
+
+		((Form_pg_temp_index) GETSTRUCT(tuple))->indisvalid = isvalid;
+
+		UpdatePgTempIndexTuple(indexoid, tuple);
+		heap_freetuple(tuple);
+	}
 }
 
 
@@ -1056,7 +1076,8 @@ index_create(Relation heapRelation,
 						isprimary, is_exclusion,
 						(constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0,
 						!concurrent && !invalid,
-						!concurrent);
+						!concurrent,
+						relpersistence);
 
 	/*
 	 * Register relcache invalidation on the indexes' heap relation, to
@@ -3583,6 +3604,9 @@ index_set_state_flags(Oid indexId, IndexStateFlagsAction action)
 	HeapTuple	indexTuple;
 	Form_pg_index indexForm;
 
+	/* This is not expected to be a global temporary index */
+	Assert(!rel_is_global_temp(indexId));
+
 	/* Open pg_index and fetch a writable copy of the index's tuple */
 	pg_index = table_open(IndexRelationId, RowExclusiveLock);
 
@@ -3942,18 +3966,26 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
 	{
 		Relation	pg_index;
 		HeapTuple	indexTuple;
+		HeapTuple	temp_indexTuple;
 		Form_pg_index indexForm;
+		Form_pg_temp_index temp_indexForm;
 		bool		index_bad;
 
+		/*
+		 * For a global temporary index, we update indisvalid in both pg_index
+		 * and pg_temp_index, so that the change applies to this session and
+		 * all future sessions.
+		 */
 		pg_index = table_open(IndexRelationId, RowExclusiveLock);
 
-		indexTuple = SearchSysCacheCopy1(INDEXRELID,
-										 ObjectIdGetDatum(indexId));
+		indexTuple = GetPgIndexAndPgTempIndexTuples(indexId, &temp_indexTuple,
+													true);
 		if (!HeapTupleIsValid(indexTuple))
 			elog(ERROR, "cache lookup failed for index %u", indexId);
 		indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+		temp_indexForm = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_indexTuple);
 
-		index_bad = (!indexForm->indisvalid ||
+		index_bad = (!GetEffective_indisvalid(indexForm, temp_indexForm) ||
 					 !indexForm->indisready ||
 					 !indexForm->indislive);
 		if (index_bad ||
@@ -3964,9 +3996,13 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
 			else if (index_bad)
 				indexForm->indcheckxmin = true;
 			indexForm->indisvalid = true;
+			if (temp_indexForm != NULL)
+				temp_indexForm->indisvalid = true;
 			indexForm->indisready = true;
 			indexForm->indislive = true;
 			CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
+			if (HeapTupleIsValid(temp_indexTuple))
+				UpdatePgTempIndexTuple(indexId, temp_indexTuple);
 
 			/*
 			 * Invalidate the relcache for the table, so that after we commit
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index f309f82d995..fba240a1167 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -1,7 +1,8 @@
 /*-------------------------------------------------------------------------
  *
  * pg_temp_class.c
- *	  routines to support manipulation of the pg_temp_class relation
+ *	  routines to support manipulation of the pg_temp_class and
+ *	  pg_temp_index relations
  *
  * The pg_temp_class system catalog table is a global temporary table that
  * stores local overrides to various fields from the pg_class table for the
@@ -34,9 +35,14 @@
  * of any non-read-only command and when committing a transaction or
  * subtransaction.
  *
- * NB: All reads and writes to pg_temp_class by backend code must go
- * through the functions defined here (though user SQL queries may read it
- * normally).
+ * Likewise pg_temp_index is the global temporary system catalog table that
+ * stores local overrides to fields (actually just indisvalid) in the
+ * pg_index table for the duration of the current session. It requires
+ * similar treatment, so for convenience we manage that here too.
+ *
+ * NB: All reads and writes to pg_temp_class and pg_temp_index by backend
+ * code must go through the functions defined here (though user SQL queries
+ * may read them normally).
  *
  * Copyright (c) 2026, PostgreSQL Global Development Group
  *
@@ -54,29 +60,35 @@
 #include "access/xact.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "miscadmin.h"
 #include "storage/proc.h"
 #include "utils/fmgroids.h"
 #include "utils/hsearch.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
 
 /*
- * Have we opened and initialized pg_temp_class in this session?
+ * Have we opened and initialized pg_temp_class and pg_temp_index in this
+ * session?
  */
 static bool pg_temp_class_opened = false;
+static bool pg_temp_index_opened = false;
 
 /*
- * Subtransaction ID in which pg_temp_class was opened, if it was opened in
- * the current transaction, else zero.
+ * Subtransaction IDs in which pg_temp_class and pg_temp_index were opened, if
+ * they were opened in the current transaction, else zero.
  */
 static SubTransactionId pg_temp_class_subid = InvalidSubTransactionId;
+static SubTransactionId pg_temp_index_subid = InvalidSubTransactionId;
 
-/* Cached copy of the pg_temp_class tuple descriptor */
+/* Cached copies of the pg_temp_class and pg_temp_index tuple descriptors */
 static TupleDesc pg_temp_class_tupdesc = NULL;
+static TupleDesc pg_temp_index_tupdesc = NULL;
 
 /*
- * Pending inserts to pg_temp_class.
+ * Pending inserts to pg_temp_class and pg_temp_index.
  *
  * Here, created_subid is the ID of the subtransaction that first added the
  * pending insert, inserted_subid is the ID of the subtransaction that flushed
@@ -93,7 +105,10 @@ typedef struct PendingInsert
 	SubTransactionId deleted_subid; /* deleted with another subid set */
 } PendingInsert;
 
-static HTAB *pending_inserts = NULL;
+static HTAB *pending_class_inserts = NULL;
+static HTAB *pending_index_inserts = NULL;
+
+/* Do we have any pending inserts of either kind that need flushing? */
 static bool have_inserts_to_flush = false;
 
 /* Memory context for all tuples pending insert */
@@ -128,27 +143,62 @@ open_pg_temp_class(LOCKMODE lockmode)
 }
 
 /*
- * init_pending_inserts_hashtable
+ * open_pg_temp_index
+ *
+ *	Open pg_temp_index and make a note of the subtranscation ID, if this is
+ *	the first time opening it.
+ */
+static Relation
+open_pg_temp_index(LOCKMODE lockmode)
+{
+	Relation	pg_temp_index;
+
+	pg_temp_index = table_open(TempIndexRelationId, lockmode);
+	if (!pg_temp_index_opened)
+	{
+		pg_temp_index_opened = true;
+		pg_temp_index_subid = GetCurrentSubTransactionId();
+	}
+	return pg_temp_index;
+}
+
+/*
+ * init_pending_inserts_hashtables
  *
- *	Initialize the pending inserts hashtable, if not already done.
+ *	Initialize the pending inserts hashtables, if not already done.
  */
 static void
-init_pending_inserts_hashtable(void)
+init_pending_inserts_hashtables(void)
 {
-	if (pending_inserts == NULL)
+	if (pending_class_inserts == NULL)
 	{
 		HASHCTL		ctl;
 
-		/* Create the hash table */
+		/* Create the hash table for pending pg_temp_class inserts */
 		ctl.keysize = sizeof(Oid);
 		ctl.entrysize = sizeof(PendingInsert);
 
-		pending_inserts = hash_create("Pending pg_temp_class inserts",
-									  128, &ctl, HASH_ELEM | HASH_BLOBS);
+		pending_class_inserts = hash_create("Pending pg_temp_class inserts",
+											128, &ctl, HASH_ELEM | HASH_BLOBS);
+	}
+
+	if (pending_index_inserts == NULL)
+	{
+		HASHCTL		ctl;
+
+		/* Create the hash table for pending pg_temp_index inserts */
+		ctl.keysize = sizeof(Oid);
+		ctl.entrysize = sizeof(PendingInsert);
 
-		/* Create a separate memory context for all tuples in it */
+		pending_index_inserts = hash_create("Pending pg_temp_index inserts",
+											128, &ctl, HASH_ELEM | HASH_BLOBS);
+	}
+
+	if (pending_inserts_tupctx == NULL)
+	{
+		/* Create a separate memory context for all pending tuples */
 		pending_inserts_tupctx = AllocSetContextCreate(TopMemoryContext,
-													   "Pending pg_temp_class tuples",
+													   "Pending pg_temp_class/index tuples",
 													   ALLOCSET_DEFAULT_SIZES);
 	}
 }
@@ -207,6 +257,39 @@ get_pg_temp_class_tupdesc(void)
 	return pg_temp_class_tupdesc;
 }
 
+/*
+ * get_pg_temp_index_tupdesc
+ *
+ *	Returns the tuple descriptor for pg_temp_index.
+ */
+static TupleDesc
+get_pg_temp_index_tupdesc(void)
+{
+	/* Build the tuple descriptor the first time through */
+	if (pg_temp_index_tupdesc == NULL)
+	{
+		MemoryContext oldcontext;
+		TupleDesc	tupdesc;
+
+		oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+		tupdesc = CreateTemplateTupleDesc(Natts_pg_temp_index);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_index_indexrelid,
+						   "indexrelid", OIDOID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_index_indisvalid,
+						   "indisvalid", BOOLOID, -1, 0);
+		TupleDescFinalize(tupdesc);
+
+		MemoryContextSwitchTo(oldcontext);
+
+		/* Cache it for all future use */
+		pg_temp_index_tupdesc = tupdesc;
+	}
+	return pg_temp_index_tupdesc;
+}
+
 /*
  * heap_form_pg_temp_class_tuple
  *
@@ -234,57 +317,110 @@ heap_form_pg_temp_class_tuple(Relation rel)
 }
 
 /*
- * flush_pending_pg_temp_class_inserts
+ * heap_form_pg_temp_index_tuple
+ *
+ *	Create a pg_temp_index tuple for the specified index relation.
+ */
+static HeapTuple
+heap_form_pg_temp_index_tuple(Oid indexrelid, bool indisvalid)
+{
+	Datum		values[Natts_pg_temp_index];
+	bool		nulls[Natts_pg_temp_index] = {0};
+
+	values[Anum_pg_temp_index_indexrelid - 1] = ObjectIdGetDatum(indexrelid);
+	values[Anum_pg_temp_index_indisvalid - 1] = BoolGetDatum(indisvalid);
+
+	return heap_form_tuple(get_pg_temp_index_tupdesc(), values, nulls);
+}
+
+/*
+ * flush_pending_inserts
  *
- *	Flush any pending inserts to pg_temp_class.
+ *	Flush any pending inserts to pg_temp_class and pg_temp_index.
  */
 static void
-flush_pending_pg_temp_class_inserts(void)
+flush_pending_inserts(void)
 {
 	SubTransactionId currentSubid = GetCurrentSubTransactionId();
 	Relation	pg_temp_class;
-	CatalogIndexState indstate;
+	Relation	pg_temp_index;
+	CatalogIndexState class_indstate;
+	CatalogIndexState index_indstate;
 	HASH_SEQ_STATUS status;
 	PendingInsert *entry;
 
+	/*
+	 * Open pg_temp_class, pg_temp_index, and their indexes.  Note that this
+	 * might be the first time these have been opened in the current session,
+	 * so we open them all first, in case they lead to more pending inserts.
+	 */
 	pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+	pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+
+	class_indstate = CatalogOpenIndexes(pg_temp_class);
+	index_indstate = CatalogOpenIndexes(pg_temp_index);
 
 	/*
-	 * Flush all pending inserts not already inserted, or deleted in the same
-	 * subtransaction.
+	 * Flush all pending pg_temp_class inserts not already inserted, or
+	 * deleted in the same subtransaction.
 	 */
-	indstate = CatalogOpenIndexes(pg_temp_class);
-	hash_seq_init(&status, pending_inserts);
+	hash_seq_init(&status, pending_class_inserts);
 	while ((entry = hash_seq_search(&status)) != NULL)
 	{
 		if (entry->inserted_subid == InvalidSubTransactionId &&
 			entry->deleted_subid == InvalidSubTransactionId)
 		{
-			CatalogTupleInsertWithInfo(pg_temp_class, entry->tuple, indstate);
+			CatalogTupleInsertWithInfo(pg_temp_class, entry->tuple, class_indstate);
 			entry->inserted_subid = currentSubid;
 		}
 	}
-	CatalogCloseIndexes(indstate);
+
+	/* And likewise for pending pg_temp_index inserts */
+	hash_seq_init(&status, pending_index_inserts);
+	while ((entry = hash_seq_search(&status)) != NULL)
+	{
+		if (entry->inserted_subid == InvalidSubTransactionId &&
+			entry->deleted_subid == InvalidSubTransactionId)
+		{
+			CatalogTupleInsertWithInfo(pg_temp_index, entry->tuple, index_indstate);
+			entry->inserted_subid = currentSubid;
+		}
+	}
+
+	/* Tidy up */
+	CatalogCloseIndexes(class_indstate);
+	CatalogCloseIndexes(index_indstate);
 
 	table_close(pg_temp_class, RowExclusiveLock);
+	table_close(pg_temp_index, RowExclusiveLock);
 
 	have_inserts_to_flush = false;
 }
 
 /*
- * discard_pending_pg_temp_class_inserts
+ * discard_pending_inserts
  *
- *	Discard any pending inserts to pg_temp_class.
+ *	Discard any pending inserts to pg_temp_class and pg_temp_index.
  */
 static void
-discard_pending_pg_temp_class_inserts(void)
+discard_pending_inserts(void)
 {
-	/* Just blow away the hash table and tuple memory context */
-	hash_destroy(pending_inserts);
-	MemoryContextDelete(pending_inserts_tupctx);
-
-	pending_inserts = NULL;
-	pending_inserts_tupctx = NULL;
+	/* Just blow away the hash tables and tuple memory context */
+	if (pending_class_inserts != NULL)
+	{
+		hash_destroy(pending_class_inserts);
+		pending_class_inserts = NULL;
+	}
+	if (pending_index_inserts != NULL)
+	{
+		hash_destroy(pending_index_inserts);
+		pending_index_inserts = NULL;
+	}
+	if (pending_inserts_tupctx != NULL)
+	{
+		MemoryContextDelete(pending_inserts_tupctx);
+		pending_inserts_tupctx = NULL;
+	}
 	have_inserts_to_flush = false;
 }
 
@@ -302,9 +438,9 @@ GetPgTempClassTuple(Oid relid)
 	PendingInsert *entry;
 
 	/* If there is a pending insert for this relation, just return that */
-	if (pending_inserts != NULL)
+	if (pending_class_inserts != NULL)
 	{
-		entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+		entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
 		if (entry != NULL)
 		{
 			/* Return NULL if pending insert was deleted */
@@ -330,6 +466,48 @@ GetPgTempClassTuple(Oid relid)
 	return SearchSysCacheCopy1(TEMPRELOID, ObjectIdGetDatum(relid));
 }
 
+/*
+ * GetPgTempIndexTuple
+ *
+ *	Get the pg_temp_index tuple for a global temporary index relation.
+ *
+ *	Returns NULL if the tuple could not be found.  Otherwise, the tuple
+ *	returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgTempIndexTuple(Oid indexrelid)
+{
+	PendingInsert *entry;
+
+	/* If there is a pending insert for this relation, just return that */
+	if (pending_index_inserts != NULL)
+	{
+		entry = hash_search(pending_index_inserts, &indexrelid, HASH_FIND, NULL);
+		if (entry != NULL)
+		{
+			/* Return NULL if pending insert was deleted */
+			if (entry->deleted_subid != InvalidSubTransactionId)
+				return NULL;
+
+			/* If the insert is still pending, return it */
+			if (entry->inserted_subid == InvalidSubTransactionId)
+				return heap_copytuple(entry->tuple);
+
+			/*
+			 * Otherwise, fall through and return whatever is in the database,
+			 * in case it was subsequently updated.
+			 */
+		}
+	}
+
+	/* If we haven't opened pg_temp_index yet, it must be empty */
+	if (!pg_temp_index_opened)
+		return NULL;
+
+	/* Otherwise fetch a copy of the tuple from the system caches */
+	return SearchSysCacheCopy1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+}
+
 /*
  * InsertPgTempClassTuple
  *
@@ -356,9 +534,9 @@ InsertPgTempClassTuple(Relation rel)
 	 * taking care to allocate the tuple in the long-term memory context for
 	 * pending insert tuples.
 	 */
-	init_pending_inserts_hashtable();
+	init_pending_inserts_hashtables();
 
-	entry = hash_search(pending_inserts, &relid, HASH_ENTER, &found);
+	entry = hash_search(pending_class_inserts, &relid, HASH_ENTER, &found);
 	if (found)
 		/* Should never try to re-insert the same relid */
 		elog(ERROR, "pg_temp_class tuple for relation %u already exists", relid);
@@ -373,6 +551,48 @@ InsertPgTempClassTuple(Relation rel)
 	have_inserts_to_flush = true;
 }
 
+/*
+ * InsertPgTempIndexTuple
+ *
+ *	Insert a new pg_temp_index tuple for a global temporary index relation.
+ *
+ *	This is called when a global temporary index relation is created or
+ *	accessed for the first time in a session.
+ *
+ *	Note: The new tuple is not written to the database unless and until
+ *	CommandCounterIncrement() is called for a non-read-only command, or the
+ *	(sub)transaction is committed.  However, the new tuple *is* visible to all
+ *	the functions defined here.
+ */
+void
+InsertPgTempIndexTuple(Oid indexrelid, bool indisvalid)
+{
+	PendingInsert *entry;
+	bool		found;
+	MemoryContext oldcontext;
+
+	/*
+	 * Add a new tuple for the relation to the pending inserts hash table,
+	 * taking care to allocate the tuple in the long-term memory context for
+	 * pending insert tuples.
+	 */
+	init_pending_inserts_hashtables();
+
+	entry = hash_search(pending_index_inserts, &indexrelid, HASH_ENTER, &found);
+	if (found)
+		/* Should never try to re-insert the same indexrelid */
+		elog(ERROR, "pg_temp_index tuple for index %u already exists", indexrelid);
+
+	oldcontext = MemoryContextSwitchTo(pending_inserts_tupctx);
+	entry->tuple = heap_form_pg_temp_index_tuple(indexrelid, indisvalid);
+	entry->created_subid = GetCurrentSubTransactionId();
+	entry->inserted_subid = InvalidSubTransactionId;
+	entry->deleted_subid = InvalidSubTransactionId;
+	MemoryContextSwitchTo(oldcontext);
+
+	have_inserts_to_flush = true;
+}
+
 /*
  * UpdatePgTempClassTuple
  *
@@ -392,11 +612,11 @@ UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple)
 	 * inserted instead.  We intentionally flush to the database here in order
 	 * to trigger cache invalidation for this relation.
 	 */
-	if (pending_inserts != NULL)
+	if (pending_class_inserts != NULL)
 	{
 		PendingInsert *entry;
 
-		entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+		entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
 		if (entry != NULL)
 		{
 			/* Entry must not have been deleted in the same subtransaction */
@@ -452,11 +672,11 @@ UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple)
 	 * inserted instead.  We intentionally flush to the database here in order
 	 * to trigger cache invalidation for this relation.
 	 */
-	if (pending_inserts != NULL)
+	if (pending_class_inserts != NULL)
 	{
 		PendingInsert *entry;
 
-		entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+		entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
 		if (entry != NULL)
 		{
 			/* Entry must not have been deleted in the same subtransaction */
@@ -497,6 +717,63 @@ UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple)
 	table_close(pg_temp_class, RowExclusiveLock);
 }
 
+/*
+ * UpdatePgTempIndexTuple
+ *
+ *	Update the pg_temp_index tuple for a global temporary index relation.
+ */
+void
+UpdatePgTempIndexTuple(Oid indexrelid, HeapTuple newtuple)
+{
+	Relation	pg_temp_index;
+	HeapTuple	oldtuple;
+
+	/*
+	 * If there is a pending insert for this relation that hasn't been
+	 * flushed, then insert the new tuple instead, and mark the pending entry
+	 * as inserted, so subsequent fetches retrieve it from the database.  On
+	 * subtransaction rollback, the pending entry will be unmarked, and may be
+	 * inserted instead.  We intentionally flush to the database here in order
+	 * to trigger cache invalidation for this relation.
+	 */
+	if (pending_index_inserts != NULL)
+	{
+		PendingInsert *entry;
+
+		entry = hash_search(pending_index_inserts, &indexrelid, HASH_FIND, NULL);
+		if (entry != NULL)
+		{
+			/* Entry must not have been deleted in the same subtransaction */
+			if (entry->deleted_subid != InvalidSubTransactionId)
+				elog(ERROR,
+					 "pending insert for global temp index %u was deleted",
+					 indexrelid);
+
+			if (entry->inserted_subid == InvalidSubTransactionId)
+			{
+				/* Insert the new version of the tuple */
+				pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+				CatalogTupleInsert(pg_temp_index, newtuple);
+				table_close(pg_temp_index, RowExclusiveLock);
+				entry->inserted_subid = GetCurrentSubTransactionId();
+				return;
+			}
+		}
+	}
+
+	/* Otherwise, update the pg_temp_index tuple directly */
+	pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+
+	oldtuple = SearchSysCache1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+	if (!HeapTupleIsValid(oldtuple))
+		elog(ERROR, "cache lookup failed for global temp index %u", indexrelid);
+
+	CatalogTupleUpdate(pg_temp_index, &oldtuple->t_self, newtuple);
+	ReleaseSysCache(oldtuple);
+
+	table_close(pg_temp_index, RowExclusiveLock);
+}
+
 /*
  * DeletePgTempClassTuple
  *
@@ -509,11 +786,11 @@ DeletePgTempClassTuple(Oid relid)
 	HeapTuple	oldtuple;
 
 	/* If there is a pending insert for this relation, mark it deleted */
-	if (pending_inserts != NULL)
+	if (pending_class_inserts != NULL)
 	{
 		PendingInsert *entry;
 
-		entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+		entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
 		if (entry != NULL)
 		{
 			/* Entry must not already be marked deleted in this subxact */
@@ -543,6 +820,52 @@ DeletePgTempClassTuple(Oid relid)
 	table_close(pg_temp_class, RowExclusiveLock);
 }
 
+/*
+ * DeletePgTempIndexTuple
+ *
+ *	Delete the pg_temp_index tuple for a global temporary index relation.
+ */
+void
+DeletePgTempIndexTuple(Oid indexrelid)
+{
+	Relation	pg_temp_index;
+	HeapTuple	oldtuple;
+
+	/* If there is a pending insert for this relation, mark it deleted */
+	if (pending_index_inserts != NULL)
+	{
+		PendingInsert *entry;
+
+		entry = hash_search(pending_index_inserts, &indexrelid, HASH_FIND, NULL);
+		if (entry != NULL)
+		{
+			/* Entry must not already be marked deleted in this subxact */
+			if (entry->deleted_subid != InvalidSubTransactionId)
+				elog(ERROR,
+					 "pending insert for global temp relation %u already deleted",
+					 indexrelid);
+
+			entry->deleted_subid = GetCurrentSubTransactionId();
+
+			/* If not inserted, nothing to do, else need to delete it */
+			if (entry->inserted_subid == InvalidSubTransactionId)
+				return;
+		}
+	}
+
+	/* Otherwise, delete the tuple from pg_temp_index directly */
+	pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+
+	oldtuple = SearchSysCache1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+	if (!HeapTupleIsValid(oldtuple))
+		elog(ERROR, "cache lookup failed for global temp index %u", indexrelid);
+
+	CatalogTupleDelete(pg_temp_index, &oldtuple->t_self);
+	ReleaseSysCache(oldtuple);
+
+	table_close(pg_temp_index, RowExclusiveLock);
+}
+
 /*
  * GetPgClassAndPgTempClassTuples
  *
@@ -585,6 +908,39 @@ GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
 	return tuple;
 }
 
+/*
+ * GetPgIndexAndPgTempIndexTuples
+ *
+ *	Get the pg_index tuple for an index relation, and if it's a global
+ *	temporary index relation, also get the corresponding pg_temp_index tuple,
+ *	if present.
+ *
+ *	Returns NULL if the pg_index tuple could not be found.  Otherwise, the
+ *	tuple(s) returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgIndexAndPgTempIndexTuples(Oid indexrelid, HeapTuple *temp_tuple,
+							   bool check_temp)
+{
+	HeapTuple	tuple;
+
+	/* Get a copy of the pg_index tuple */
+	tuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexrelid));
+
+	if (HeapTupleIsValid(tuple) &&
+		rel_is_global_temp(((Form_pg_index) GETSTRUCT(tuple))->indexrelid))
+	{
+		/* Get the pg_temp_index tuple, and check it exists, if requested */
+		*temp_tuple = GetPgTempIndexTuple(indexrelid);
+		if (check_temp && !HeapTupleIsValid(*temp_tuple))
+			elog(ERROR, "cache lookup failed for global temp index %u", indexrelid);
+	}
+	else
+		*temp_tuple = NULL;
+
+	return tuple;
+}
+
 /*
  * GetEffectivePgClassTuple
  *
@@ -626,6 +982,47 @@ GetEffectivePgClassTuple(Oid relid)
 	return tuple;
 }
 
+/*
+ * GetEffectivePgIndexTuple
+ *
+ *	Get the effective pg_index tuple for an index relation.
+ *
+ *	This will fetch the pg_index tuple for the relation and then, if it's a
+ *	global temporary relation, fetch the corresponding pg_temp_index tuple and
+ *	use the values in it to override the corresponding values in the pg_index
+ *	tuple (currently just indisvalid).  Thus, the result represents the
+ *	effective state of the index relation in this session.
+ *
+ *	For a global temporary index relation that has not yet been opened in this
+ *	session, there will be no pg_temp_index tuple, and the pg_index tuple will
+ *	be returned unchanged.
+ *
+ *	Returns NULL if the pg_index tuple could not be found.  Otherwise, the
+ *	tuple returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetEffectivePgIndexTuple(Oid indexrelid)
+{
+	HeapTuple	tuple;
+	HeapTuple	temp_tuple;
+	Form_pg_index indexform;
+	Form_pg_temp_index temp_indexform;
+
+	/*
+	 * Get the pg_index and pg_temp_index tuples.  If we have the latter, use
+	 * it to update the former.
+	 */
+	tuple = GetPgIndexAndPgTempIndexTuples(indexrelid, &temp_tuple, false);
+
+	if (HeapTupleIsValid(tuple) && HeapTupleIsValid(temp_tuple))
+	{
+		indexform = (Form_pg_index) GETSTRUCT(tuple);
+		temp_indexform = (Form_pg_temp_index) GETSTRUCT(temp_tuple);
+		indexform->indisvalid = temp_indexform->indisvalid;
+	}
+	return tuple;
+}
+
 /*
  * UpdateTempFrozenXids
  *
@@ -655,10 +1052,10 @@ UpdateTempFrozenXids(void)
 	min_relfrozenxid = InvalidTransactionId;
 	min_relminmxid = InvalidMultiXactId;
 
-	/* Processing any pending inserts */
-	if (pending_inserts != NULL)
+	/* Processing any pending pg_temp_class inserts */
+	if (pending_class_inserts != NULL)
 	{
-		hash_seq_init(&status, pending_inserts);
+		hash_seq_init(&status, pending_class_inserts);
 		while ((entry = hash_seq_search(&status)) != NULL)
 		{
 			temp_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
@@ -740,7 +1137,7 @@ void
 PreCCI_PgTempClass(void)
 {
 	if (have_inserts_to_flush)
-		flush_pending_pg_temp_class_inserts();
+		flush_pending_inserts();
 }
 
 /*
@@ -752,7 +1149,7 @@ void
 PreCommit_PgTempClass(void)
 {
 	if (have_inserts_to_flush)
-		flush_pending_pg_temp_class_inserts();
+		flush_pending_inserts();
 }
 
 /*
@@ -764,7 +1161,7 @@ void
 PreSubCommit_PgTempClass(void)
 {
 	if (have_inserts_to_flush)
-		flush_pending_pg_temp_class_inserts();
+		flush_pending_inserts();
 }
 
 /*
@@ -783,13 +1180,17 @@ AtEOXact_PgTempClass(bool isCommit)
 		pg_temp_class_opened = false;
 	pg_temp_class_subid = InvalidSubTransactionId;
 
+	/* Likewise for pg_temp_index */
+	if (!isCommit && pg_temp_index_subid != InvalidSubTransactionId)
+		pg_temp_index_opened = false;
+	pg_temp_index_subid = InvalidSubTransactionId;
+
 	/*
-	 * Blow away the pending inserts hash table.  On commit, there should be
+	 * Blow away the pending inserts hash tables.  On commit, there should be
 	 * no remaining inserts to flush, but on rollback, there may be.
 	 */
 	Assert(!(isCommit && have_inserts_to_flush));
-	if (pending_inserts != NULL)
-		discard_pending_pg_temp_class_inserts();
+	discard_pending_inserts();
 
 	/*
 	 * On commit, save any new tempfrozenxid and tempminmxid values to our
@@ -803,6 +1204,56 @@ AtEOXact_PgTempClass(bool isCommit)
 	min_frozenxids_updated = false;
 }
 
+/*
+ * AtEOSubXact_PendingInsertCleanup
+ *
+ *	Sub-transaction commit or abort cleanup for a single pg_temp_class or
+ *	pg_temp_index pending insert entry.
+ */
+static void
+AtEOSubXact_PendingInsertCleanup(HTAB *pending_inserts, PendingInsert *entry,
+								 bool isCommit, SubTransactionId mySubid,
+								 SubTransactionId parentSubid)
+{
+	/*
+	 * Was the pending insert entry created in the current subtransaction?
+	 *
+	 * During subcommit, mark is as created in the parent, instead, as long as
+	 * it has not been deleted.  Otherwise, discard the entry.
+	 */
+	if (entry->created_subid == mySubid)
+	{
+		Assert(entry->deleted_subid == mySubid ||
+			   entry->deleted_subid == InvalidSubTransactionId);
+
+		if (isCommit && entry->deleted_subid == InvalidSubTransactionId)
+			entry->created_subid = parentSubid;
+		else
+			hash_search(pending_inserts, &entry->relid, HASH_REMOVE, NULL);
+	}
+
+	/* Update the inserted subid (may rollback flush) */
+	if (entry->inserted_subid == mySubid)
+	{
+		if (isCommit)
+			entry->inserted_subid = parentSubid;
+		else
+		{
+			entry->inserted_subid = InvalidSubTransactionId;
+			have_inserts_to_flush = true;
+		}
+	}
+
+	/* Update the deleted subid */
+	if (entry->deleted_subid == mySubid)
+	{
+		if (isCommit)
+			entry->deleted_subid = parentSubid;
+		else
+			entry->deleted_subid = InvalidSubTransactionId;
+	}
+}
+
 /*
  * AtEOSubXact_PgTempClass
  *
@@ -812,6 +1263,9 @@ void
 AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
 						SubTransactionId parentSubid)
 {
+	HASH_SEQ_STATUS status;
+	PendingInsert *entry;
+
 	/*
 	 * Was pg_temp_class first opened and initialized in the current
 	 * subtransaction?
@@ -830,54 +1284,37 @@ AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
 		}
 	}
 
-	/*
-	 * Tidy up any pending inserts.
-	 */
-	if (pending_inserts != NULL)
+	/* Likewise for pg_temp_index */
+	if (pg_temp_index_subid == mySubid)
 	{
-		HASH_SEQ_STATUS status;
-		PendingInsert *entry;
+		if (isCommit)
+			pg_temp_index_subid = parentSubid;
+		else
+		{
+			pg_temp_index_opened = false;
+			pg_temp_index_subid = InvalidSubTransactionId;
+		}
+	}
 
-		hash_seq_init(&status, pending_inserts);
+	/* Tidy up any pending pg_temp_class inserts */
+	if (pending_class_inserts != NULL)
+	{
+		hash_seq_init(&status, pending_class_inserts);
 		while ((entry = hash_seq_search(&status)) != NULL)
 		{
-			/*
-			 * Was the pending entry created in the current subtransaction?
-			 *
-			 * During subcommit, mark is as created in the parent, instead, as
-			 * long as it has not been deleted.  Otherwise, discard the entry.
-			 */
-			if (entry->created_subid == mySubid)
-			{
-				Assert(entry->deleted_subid == mySubid ||
-					   entry->deleted_subid == InvalidSubTransactionId);
-
-				if (isCommit && entry->deleted_subid == InvalidSubTransactionId)
-					entry->created_subid = parentSubid;
-				else
-					hash_search(pending_inserts, &entry->relid, HASH_REMOVE, NULL);
-			}
-
-			/* Update the inserted subid (may rollback flush) */
-			if (entry->inserted_subid == mySubid)
-			{
-				if (isCommit)
-					entry->inserted_subid = parentSubid;
-				else
-				{
-					entry->inserted_subid = InvalidSubTransactionId;
-					have_inserts_to_flush = true;
-				}
-			}
+			AtEOSubXact_PendingInsertCleanup(pending_class_inserts, entry,
+											 isCommit, mySubid, parentSubid);
+		}
+	}
 
-			/* Update the deleted subid */
-			if (entry->deleted_subid == mySubid)
-			{
-				if (isCommit)
-					entry->deleted_subid = parentSubid;
-				else
-					entry->deleted_subid = InvalidSubTransactionId;
-			}
+	/* Likewise for pg_temp_index */
+	if (pending_index_inserts != NULL)
+	{
+		hash_seq_init(&status, pending_index_inserts);
+		while ((entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOSubXact_PendingInsertCleanup(pending_index_inserts, entry,
+											 isCommit, mySubid, parentSubid);
 		}
 	}
 }
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 8baac58ef81..beb90ba09a7 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -37,6 +37,8 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
@@ -269,7 +271,7 @@ CheckIndexCompatible(Oid oldId,
 					  0, NULL);
 
 	/* Get the soon-obsolete pg_index tuple. */
-	tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
+	tuple = GetEffectivePgIndexTuple(oldId);
 	if (!HeapTupleIsValid(tuple))
 		elog(ERROR, "cache lookup failed for index %u", oldId);
 	indexForm = (Form_pg_index) GETSTRUCT(tuple);
@@ -282,7 +284,7 @@ CheckIndexCompatible(Oid oldId,
 		  heap_attisnull(tuple, Anum_pg_index_indexprs, NULL) &&
 		  indexForm->indisvalid))
 	{
-		ReleaseSysCache(tuple);
+		heap_freetuple(tuple);
 		return false;
 	}
 
@@ -299,7 +301,7 @@ CheckIndexCompatible(Oid oldId,
 	ret = (memcmp(old_indclass->values, opclassIds, old_natts * sizeof(Oid)) == 0 &&
 		   memcmp(old_indcollation->values, collationIds, old_natts * sizeof(Oid)) == 0);
 
-	ReleaseSysCache(tuple);
+	heap_freetuple(tuple);
 
 	if (!ret)
 		return false;
@@ -1581,19 +1583,36 @@ DefineIndex(ParseState *pstate,
 			{
 				Relation	pg_index = table_open(IndexRelationId, RowExclusiveLock);
 				HeapTuple	tup,
-							newtup;
+							temp_tup;
+				Form_pg_index form;
+				Form_pg_temp_index temp_form;
 
-				tup = SearchSysCache1(INDEXRELID,
-									  ObjectIdGetDatum(indexRelationId));
+				/*
+				 * For a global temporary index, we update indisvalid in both
+				 * pg_index and pg_temp_index, so that the change applies to
+				 * this session and all future sessions.
+				 */
+				tup = GetPgIndexAndPgTempIndexTuples(indexRelationId,
+													 &temp_tup, true);
 				if (!HeapTupleIsValid(tup))
 					elog(ERROR, "cache lookup failed for index %u",
 						 indexRelationId);
-				newtup = heap_copytuple(tup);
-				((Form_pg_index) GETSTRUCT(newtup))->indisvalid = false;
-				CatalogTupleUpdate(pg_index, &tup->t_self, newtup);
-				ReleaseSysCache(tup);
+				form = (Form_pg_index) GETSTRUCT(tup);
+				temp_form = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_tup);
+
+				form->indisvalid = false;
+				if (temp_form != NULL)
+					temp_form->indisvalid = false;
+
+				CatalogTupleUpdate(pg_index, &tup->t_self, tup);
+				if (HeapTupleIsValid(temp_tup))
+				{
+					UpdatePgTempIndexTuple(indexRelationId, temp_tup);
+					heap_freetuple(temp_tup);
+				}
+
+				heap_freetuple(tup);
 				table_close(pg_index, RowExclusiveLock);
-				heap_freetuple(newtup);
 
 				/*
 				 * CCI here to make this update visible, in case this recurses
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 0c704a531b2..2ec0bcaca29 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -868,8 +868,7 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 	{
 		Oid			thisIndexOid = lfirst_oid(index);
 
-		indexTuple = SearchSysCacheCopy1(INDEXRELID,
-										 ObjectIdGetDatum(thisIndexOid));
+		indexTuple = GetEffectivePgIndexTuple(thisIndexOid);
 		if (!HeapTupleIsValid(indexTuple))
 			elog(ERROR, "cache lookup failed for index %u", thisIndexOid);
 		indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d3394fffd61..4dff3969097 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -55,6 +55,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -1859,7 +1860,7 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
 		Form_pg_index indexform;
 		bool		indisvalid;
 
-		locTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(relOid));
+		locTuple = GetEffectivePgIndexTuple(relOid);
 		if (!HeapTupleIsValid(locTuple))
 		{
 			ReleaseSysCache(tuple);
@@ -1868,7 +1869,7 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
 
 		indexform = (Form_pg_index) GETSTRUCT(locTuple);
 		indisvalid = indexform->indisvalid;
-		ReleaseSysCache(locTuple);
+		heap_freetuple(locTuple);
 
 		/* Mark object as being an invalid index of system catalogs */
 		if (!indisvalid)
@@ -14004,7 +14005,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 	{
 		Oid			indexoid = lfirst_oid(indexoidscan);
 
-		indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+		indexTuple = GetEffectivePgIndexTuple(indexoid);
 		if (!HeapTupleIsValid(indexTuple))
 			elog(ERROR, "cache lookup failed for index %u", indexoid);
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
@@ -14024,7 +14025,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 			*indexOid = indexoid;
 			break;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 	}
 
 	list_free(indexoidlist);
@@ -14062,7 +14063,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	*pk_has_without_overlaps = indexStruct->indisexclusion;
 
-	ReleaseSysCache(indexTuple);
+	heap_freetuple(indexTuple);
 
 	return i;
 }
@@ -14125,7 +14126,7 @@ transformFkeyCheckAttrs(Relation pkrel,
 		Form_pg_index indexStruct;
 
 		indexoid = lfirst_oid(indexoidscan);
-		indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+		indexTuple = GetEffectivePgIndexTuple(indexoid);
 		if (!HeapTupleIsValid(indexTuple))
 			elog(ERROR, "cache lookup failed for index %u", indexoid);
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
@@ -14201,7 +14202,7 @@ transformFkeyCheckAttrs(Relation pkrel,
 			if (found)
 				*pk_has_without_overlaps = indexStruct->indisexclusion;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 		if (found)
 			break;
 	}
@@ -22489,14 +22490,13 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
 		HeapTuple	indTup;
 		Form_pg_index indexForm;
 
-		indTup = SearchSysCache1(INDEXRELID,
-								 ObjectIdGetDatum(inhForm->inhrelid));
+		indTup = GetEffectivePgIndexTuple(inhForm->inhrelid);
 		if (!HeapTupleIsValid(indTup))
 			elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid);
 		indexForm = (Form_pg_index) GETSTRUCT(indTup);
 		if (indexForm->indisvalid)
 			tuples += 1;
-		ReleaseSysCache(indTup);
+		heap_freetuple(indTup);
 	}
 
 	/* Done with pg_inherits */
@@ -22511,20 +22511,35 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
 	{
 		Relation	idxRel;
 		HeapTuple	indTup;
+		HeapTuple	temp_indTup;
 		Form_pg_index indexForm;
+		Form_pg_temp_index temp_indexForm;
 
+		/*
+		 * For a global temporary index, we update indisvalid in both pg_index
+		 * and pg_temp_index, so that the change applies to this session and
+		 * all future sessions.
+		 */
 		idxRel = table_open(IndexRelationId, RowExclusiveLock);
-		indTup = SearchSysCacheCopy1(INDEXRELID,
-									 ObjectIdGetDatum(RelationGetRelid(partedIdx)));
+		indTup = GetPgIndexAndPgTempIndexTuples(RelationGetRelid(partedIdx),
+												&temp_indTup, true);
 		if (!HeapTupleIsValid(indTup))
 			elog(ERROR, "cache lookup failed for index %u",
 				 RelationGetRelid(partedIdx));
 		indexForm = (Form_pg_index) GETSTRUCT(indTup);
+		temp_indexForm = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_indTup);
 
 		indexForm->indisvalid = true;
+		if (temp_indexForm != NULL)
+			temp_indexForm->indisvalid = true;
 		updated = true;
 
 		CatalogTupleUpdate(idxRel, &indTup->t_self, indTup);
+		if (HeapTupleIsValid(temp_indTup))
+		{
+			UpdatePgTempIndexTuple(RelationGetRelid(partedIdx), temp_indTup);
+			heap_freetuple(temp_indTup);
+		}
 
 		table_close(idxRel, RowExclusiveLock);
 		heap_freetuple(indTup);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index ecc34f07e36..d35b6c6ce12 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3965,13 +3965,13 @@ get_index_isvalid(Oid index_oid)
 	HeapTuple	tuple;
 	Form_pg_index rd_index;
 
-	tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(index_oid));
+	tuple = GetEffectivePgIndexTuple(index_oid);
 	if (!HeapTupleIsValid(tuple))
 		elog(ERROR, "cache lookup failed for index %u", index_oid);
 
 	rd_index = (Form_pg_index) GETSTRUCT(tuple);
 	isvalid = rd_index->indisvalid;
-	ReleaseSysCache(tuple);
+	heap_freetuple(tuple);
 
 	return isvalid;
 }
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7ea08c2428f..fd46606928a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -63,6 +63,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/pg_temp_statistic.h"
 #include "catalog/pg_temp_statistic_ext_data.h"
 #include "catalog/pg_trigger.h"
@@ -1531,6 +1532,23 @@ RelationInitIndexAccessInfo(Relation relation)
 	MemoryContextSwitchTo(oldcontext);
 	ReleaseSysCache(tuple);
 
+	/*
+	 * For global temporary indexes, update indisvalid from pg_temp_index.
+	 * This won't work for system catalog indexes, because pg_temp_index may
+	 * not be loaded at this point, but they shouldn't be invalid anyway.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(relation) && !IsCatalogRelation(relation))
+	{
+		tuple = GetPgTempIndexTuple(RelationGetRelid(relation));
+		if (HeapTupleIsValid(tuple))
+		{
+			Form_pg_temp_index temp_form = (Form_pg_temp_index) GETSTRUCT(tuple);
+
+			relation->rd_index->indisvalid = temp_form->indisvalid;
+			heap_freetuple(tuple);
+		}
+	}
+
 	/*
 	 * Look up the index's access method, save the OID of its handler function
 	 */
@@ -2441,8 +2459,7 @@ RelationReloadIndexInfo(Relation relation)
 		HeapTuple	tuple;
 		Form_pg_index index;
 
-		tuple = SearchSysCache1(INDEXRELID,
-								ObjectIdGetDatum(RelationGetRelid(relation)));
+		tuple = GetEffectivePgIndexTuple(RelationGetRelid(relation));
 		if (!HeapTupleIsValid(tuple))
 			elog(ERROR, "cache lookup failed for index %u",
 				 RelationGetRelid(relation));
@@ -2470,7 +2487,7 @@ RelationReloadIndexInfo(Relation relation)
 		HeapTupleHeaderSetXmin(relation->rd_indextuple->t_data,
 							   HeapTupleHeaderGetXmin(tuple->t_data));
 
-		ReleaseSysCache(tuple);
+		heap_freetuple(tuple);
 	}
 
 	/* Okay, now it's valid again */
@@ -5032,6 +5049,8 @@ RelationGetIndexList(Relation relation)
 	while (HeapTupleIsValid(htup = systable_getnext(indscan)))
 	{
 		Form_pg_index index = (Form_pg_index) GETSTRUCT(htup);
+		HeapTuple	temp_htup;
+		bool		indisvalid;
 
 		/*
 		 * Ignore any indexes that are currently being dropped.  This will
@@ -5055,6 +5074,21 @@ RelationGetIndexList(Relation relation)
 			!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
 			continue;
 
+		/*
+		 * Global temporary indexes may override indexisvalid locally.
+		 */
+		indisvalid = index->indisvalid;
+		if (RELATION_IS_GLOBAL_TEMP(relation))
+		{
+			temp_htup = GetPgTempIndexTuple(index->indexrelid);
+
+			if (HeapTupleIsValid(temp_htup))
+			{
+				indisvalid = ((Form_pg_temp_index) GETSTRUCT(temp_htup))->indisvalid;
+				heap_freetuple(temp_htup);
+			}
+		}
+
 		/*
 		 * Remember primary key index, if any.  For regular tables we do this
 		 * only if the index is valid; but for partitioned tables, then we do
@@ -5066,7 +5100,7 @@ RelationGetIndexList(Relation relation)
 		 * partitioned tables.
 		 */
 		if (index->indisprimary &&
-			(index->indisvalid ||
+			(indisvalid ||
 			 relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
 		{
 			pkeyIndex = index->indexrelid;
@@ -5076,7 +5110,7 @@ RelationGetIndexList(Relation relation)
 		if (!index->indimmediate)
 			continue;
 
-		if (!index->indisvalid)
+		if (!indisvalid)
 			continue;
 
 		/* remember explicitly chosen replica index */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 8ce4decb91e..3e9c2b85517 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2406,6 +2406,20 @@ describeOneTableDetails(const char *schemaname,
 			char	   *indamname = PQgetvalue(result, 0, 8);
 			char	   *indtable = PQgetvalue(result, 0, 9);
 			char	   *indpred = PQgetvalue(result, 0, 10);
+			PGresult   *temp_result = NULL;
+
+			if (pset.sversion >= 200000)
+			{
+				printfPQExpBuffer(&buf, "/* %s */\n", _("Get temp index details"));
+				appendPQExpBuffer(&buf,
+								  "SELECT i.indisvalid\n"
+								  "FROM pg_catalog.pg_temp_index i\n"
+								  "WHERE i.indexrelid = '%s'", oid);
+
+				temp_result = PSQLexec(buf.data);
+				if (temp_result && PQntuples(temp_result) == 1)
+					indisvalid = PQgetvalue(temp_result, 0, 0);
+			}
 
 			if (strcmp(indisprimary, "t") == 0)
 				printfPQExpBuffer(&tmpbuf, _("primary key, "));
@@ -2450,6 +2464,8 @@ describeOneTableDetails(const char *schemaname,
 			if (tableinfo.relkind == RELKIND_INDEX)
 				add_tablespace_footer(&cont, tableinfo.relkind,
 									  tableinfo.tablespace, true);
+
+			PQclear(temp_result);
 		}
 
 		PQclear(result);
@@ -2482,6 +2498,7 @@ describeOneTableDetails(const char *schemaname,
 				appendPQExpBufferStr(&buf, ", con.conperiod");
 			else
 				appendPQExpBufferStr(&buf, ", false AS conperiod");
+			appendPQExpBufferStr(&buf, ", i.indexrelid");
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n"
 							  "  LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ("
@@ -2502,67 +2519,94 @@ describeOneTableDetails(const char *schemaname,
 				printTableAddFooter(&cont, _("Indexes:"));
 				for (i = 0; i < tuples; i++)
 				{
+					char	   *indname = PQgetvalue(result, i, 0);
+					char	   *indisprimary = PQgetvalue(result, i, 1);
+					char	   *indisunique = PQgetvalue(result, i, 2);
+					char	   *indisclustered = PQgetvalue(result, i, 3);
+					char	   *indisvalid = PQgetvalue(result, i, 4);
+					char	   *indexdef = PQgetvalue(result, i, 5);
+					char	   *constraintdef = PQgetvalue(result, i, 6);
+					char	   *contype = PQgetvalue(result, i, 7);
+					char	   *condeferrable = PQgetvalue(result, i, 8);
+					char	   *condeferred = PQgetvalue(result, i, 9);
+					char	   *indisreplident = PQgetvalue(result, i, 10);
+					char	   *reltablespace = PQgetvalue(result, i, 11);
+					char	   *conperiod = PQgetvalue(result, i, 12);
+					char	   *indexrelid = PQgetvalue(result, i, 13);
+					PGresult   *temp_result = NULL;
+
+					if (pset.sversion >= 200000)
+					{
+						printfPQExpBuffer(&buf, "/* %s */\n", _("Get temp index details"));
+						appendPQExpBuffer(&buf,
+										  "SELECT i.indisvalid\n"
+										  "FROM pg_catalog.pg_temp_index i\n"
+										  "WHERE i.indexrelid = '%s'", indexrelid);
+
+						temp_result = PSQLexec(buf.data);
+						if (temp_result && PQntuples(temp_result) == 1)
+							indisvalid = PQgetvalue(temp_result, 0, 0);
+					}
+
 					/* untranslated index name */
-					printfPQExpBuffer(&buf, "    \"%s\"",
-									  PQgetvalue(result, i, 0));
+					printfPQExpBuffer(&buf, "    \"%s\"", indname);
 
 					/*
 					 * If exclusion constraint or PK/UNIQUE constraint WITHOUT
 					 * OVERLAPS, print the constraintdef
 					 */
-					if (strcmp(PQgetvalue(result, i, 7), "x") == 0 ||
-						strcmp(PQgetvalue(result, i, 12), "t") == 0)
+					if (strcmp(contype, "x") == 0 ||
+						strcmp(conperiod, "t") == 0)
 					{
-						appendPQExpBuffer(&buf, " %s",
-										  PQgetvalue(result, i, 6));
+						appendPQExpBuffer(&buf, " %s", constraintdef);
 					}
 					else
 					{
-						const char *indexdef;
-						const char *usingpos;
+						char	   *usingpos;
 
 						/* Label as primary key or unique (but not both) */
-						if (strcmp(PQgetvalue(result, i, 1), "t") == 0)
+						if (strcmp(indisprimary, "t") == 0)
 							appendPQExpBufferStr(&buf, " PRIMARY KEY,");
-						else if (strcmp(PQgetvalue(result, i, 2), "t") == 0)
+						else if (strcmp(indisunique, "t") == 0)
 						{
-							if (strcmp(PQgetvalue(result, i, 7), "u") == 0)
+							if (strcmp(contype, "u") == 0)
 								appendPQExpBufferStr(&buf, " UNIQUE CONSTRAINT,");
 							else
 								appendPQExpBufferStr(&buf, " UNIQUE,");
 						}
 
 						/* Everything after "USING" is echoed verbatim */
-						indexdef = PQgetvalue(result, i, 5);
 						usingpos = strstr(indexdef, " USING ");
 						if (usingpos)
 							indexdef = usingpos + 7;
 						appendPQExpBuffer(&buf, " %s", indexdef);
 
 						/* Need these for deferrable PK/UNIQUE indexes */
-						if (strcmp(PQgetvalue(result, i, 8), "t") == 0)
+						if (strcmp(condeferrable, "t") == 0)
 							appendPQExpBufferStr(&buf, " DEFERRABLE");
 
-						if (strcmp(PQgetvalue(result, i, 9), "t") == 0)
+						if (strcmp(condeferred, "t") == 0)
 							appendPQExpBufferStr(&buf, " INITIALLY DEFERRED");
 					}
 
 					/* Add these for all cases */
-					if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+					if (strcmp(indisclustered, "t") == 0)
 						appendPQExpBufferStr(&buf, " CLUSTER");
 
-					if (strcmp(PQgetvalue(result, i, 4), "t") != 0)
+					if (strcmp(indisvalid, "t") != 0)
 						appendPQExpBufferStr(&buf, " INVALID");
 
-					if (strcmp(PQgetvalue(result, i, 10), "t") == 0)
+					if (strcmp(indisreplident, "t") == 0)
 						appendPQExpBufferStr(&buf, " REPLICA IDENTITY");
 
 					printTableAddFooter(&cont, buf.data);
 
 					/* Print tablespace of the index on the same line */
 					add_tablespace_footer(&cont, RELKIND_INDEX,
-										  atooid(PQgetvalue(result, i, 11)),
+										  atooid(reltablespace),
 										  false);
+
+					PQclear(temp_result);
 				}
 			}
 			PQclear(result);
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index f60a6454df2..71ea60228d0 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -88,6 +88,7 @@ CATALOG_HEADERS := \
 	pg_propgraph_label_property.h \
 	pg_propgraph_property.h \
 	pg_temp_class.h \
+	pg_temp_index.h \
 	pg_temp_statistic.h \
 	pg_temp_statistic_ext_data.h
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 7599731de7d..d6ea84a2d46 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -75,6 +75,7 @@ catalog_headers = [
   'pg_propgraph_label_property.h',
   'pg_propgraph_property.h',
   'pg_temp_class.h',
+  'pg_temp_index.h',
   'pg_temp_statistic.h',
   'pg_temp_statistic_ext_data.h',
 ]
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index a4629b12c9d..ba577001225 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -369,20 +369,27 @@ SetEffective_relminmxid(Form_pg_class cf, Form_pg_temp_class tf,
 
 
 extern HeapTuple GetPgTempClassTuple(Oid relid);
+extern HeapTuple GetPgTempIndexTuple(Oid indexrelid);
 
 extern void InsertPgTempClassTuple(Relation rel);
+extern void InsertPgTempIndexTuple(Oid indexrelid, bool indisvalid);
 
 extern void UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple);
-
 extern void UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple);
+extern void UpdatePgTempIndexTuple(Oid indexrelid, HeapTuple newtuple);
 
 extern void DeletePgTempClassTuple(Oid relid);
+extern void DeletePgTempIndexTuple(Oid indexrelid);
 
 extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
 												HeapTuple *temp_tuple,
 												bool check_temp);
+extern HeapTuple GetPgIndexAndPgTempIndexTuples(Oid indexrelid,
+												HeapTuple *temp_tuple,
+												bool check_temp);
 
 extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+extern HeapTuple GetEffectivePgIndexTuple(Oid indexrelid);
 
 extern void UpdateTempFrozenXids(void);
 
diff --git a/src/include/catalog/pg_temp_index.h b/src/include/catalog/pg_temp_index.h
new file mode 100644
index 00000000000..ce79bf0e2c5
--- /dev/null
+++ b/src/include/catalog/pg_temp_index.h
@@ -0,0 +1,66 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_index.h
+ *	  definition of the "temporary index" system catalog (pg_temp_index)
+ *
+ * This is a global temporary system catalog table storing session-specific
+ * information about temporary indexes.  Currently, it is only used for global
+ * temporary indexes.  The attributes (currently just indisvalid) are a subset
+ * of those from pg_index, and their values take precedence over the values
+ * from pg_index.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_index.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_INDEX_H
+#define PG_TEMP_INDEX_H
+
+#include "access/genam.h"
+#include "catalog/genbki.h"
+#include "catalog/pg_index.h"
+#include "catalog/pg_temp_index_d.h"	/* IWYU pragma: export */
+
+/* ----------------
+ *		pg_temp_index definition.  cpp turns this into
+ *		typedef struct FormData_pg_temp_index.
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+CATALOG(pg_temp_index,8092,TempIndexRelationId) BKI_TEMP_RELATION
+{
+	Oid			indexrelid BKI_LOOKUP(pg_class);	/* OID of the index */
+	bool		indisvalid;		/* is this index valid for use by queries? */
+} FormData_pg_temp_index;
+
+END_CATALOG_STRUCT
+
+/* ----------------
+ *		Form_pg_temp_index corresponds to a pointer to a tuple with
+ *		the format of pg_temp_index relation.
+ * ----------------
+ */
+typedef FormData_pg_temp_index *Form_pg_temp_index;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_index_indexrelid_index, 8093, TempIndexRelidIndexId, pg_temp_index, btree(indexrelid oid_ops));
+
+MAKE_SYSCACHE(TEMPINDEXRELID, pg_temp_index_indexrelid_index, 64);
+
+/*
+ * Get the effective value of indisvalid from pg_index and pg_temp_index tuple
+ * data.  The value from pg_temp_index (if present) takes precedence.
+ */
+static inline bool
+GetEffective_indisvalid(Form_pg_index f, Form_pg_temp_index tf)
+{
+	return tf != NULL ? tf->indisvalid : f->indisvalid;
+}
+
+#endif							/* PG_TEMP_INDEX_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index bbaa768f479..b0f632779b6 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -438,6 +438,87 @@ key|val|seq
 (1 row)
 
 
+starting permutation: ins1 ins2 idx1 sel1_idx sel2_idx analyze2 sel2_idx reidx2 sel2_idx
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step idx1: CREATE INDEX tmp_val_idx ON tmp(val);
+step sel1_idx: 
+  SET enable_seqscan = off;
+  SET enable_bitmapscan = off;
+  EXPLAIN (COSTS OFF)
+  SELECT * FROM tmp WHERE val = 's1';
+  SELECT * FROM tmp WHERE val = 's1';
+
+QUERY PLAN                         
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+  Index Cond: (val = 's1'::text)   
+(2 rows)
+
+key|val|seq
+---+---+---
+  1|s1 |  1
+(1 row)
+
+step sel2_idx: 
+  SET enable_seqscan = off;
+  SET enable_bitmapscan = off;
+  EXPLAIN (COSTS OFF)
+  SELECT * FROM tmp WHERE val = 's2';
+  SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN                  
+----------------------------
+Seq Scan on tmp             
+  Disabled: true            
+  Filter: (val = 's2'::text)
+(3 rows)
+
+key|val|seq
+---+---+---
+  1|s2 |  1
+(1 row)
+
+step analyze2: ANALYZE tmp;
+step sel2_idx: 
+  SET enable_seqscan = off;
+  SET enable_bitmapscan = off;
+  EXPLAIN (COSTS OFF)
+  SELECT * FROM tmp WHERE val = 's2';
+  SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN                  
+----------------------------
+Seq Scan on tmp             
+  Disabled: true            
+  Filter: (val = 's2'::text)
+(3 rows)
+
+key|val|seq
+---+---+---
+  1|s2 |  1
+(1 row)
+
+step reidx2: REINDEX INDEX tmp_val_idx;
+step sel2_idx: 
+  SET enable_seqscan = off;
+  SET enable_bitmapscan = off;
+  EXPLAIN (COSTS OFF)
+  SELECT * FROM tmp WHERE val = 's2';
+  SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN                         
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+  Index Cond: (val = 's2'::text)   
+(2 rows)
+
+key|val|seq
+---+---+---
+  1|s2 |  1
+(1 row)
+
+
 starting permutation: ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
 step ins1: INSERT INTO tmp VALUES (1, 's1');
 step ins2: INSERT INTO tmp VALUES (1, 's2');
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index a57fcfd3e53..b3b207acb9d 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -85,6 +85,7 @@ step get_tblspace2 {
     LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
    WHERE c.relname = 'tmp';
 }
+step analyze2 { ANALYZE tmp; }
 
 # Create test tablespace for remaining tests
 permutation create_tblspace list_tblspaces
@@ -114,6 +115,7 @@ permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
 permutation ins1 idx1 sel1_idx ins2 sel2_idx
 permutation ins1 ins2 idx1 sel1_idx sel2_idx
 permutation ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx
+permutation ins1 ins2 idx1 sel1_idx sel2_idx analyze2 sel2_idx reidx2 sel2_idx
 
 # Test local TRUNCATE
 permutation ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 10519ceee21..693d12eaad4 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -227,6 +227,109 @@ SELECT * FROM tmp2 ORDER BY a;
  2
 (1 row)
 
+DROP TABLE tmp2;
+-- Test partitioned index validity
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE INDEX tmp2_a_idx ON tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+  LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+    relname    | global_valid | local_valid 
+---------------+--------------+-------------
+ tmp2_a_idx    | t            | t
+ tmp2_p1_a_idx | t            | t
+(2 rows)
+
+\d tmp2
+Global temporary partitioned table "global_temp_tests.tmp2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition key: LIST (a)
+Indexes:
+    "tmp2_a_idx" btree (a)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_a_idx
+Partitioned index "global_temp_tests.tmp2_a_idx"
+ Column |  Type   | Key? | Definition 
+--------+---------+------+------------
+ a      | integer | yes  | a
+btree, for table "global_temp_tests.tmp2"
+Number of partitions: 1 (Use \d+ to list them.)
+
+DROP INDEX tmp2_a_idx;
+CREATE INDEX tmp2_a_idx ON ONLY tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+  LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+  relname   | global_valid | local_valid 
+------------+--------------+-------------
+ tmp2_a_idx | f            | f
+(1 row)
+
+\d tmp2
+Global temporary partitioned table "global_temp_tests.tmp2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition key: LIST (a)
+Indexes:
+    "tmp2_a_idx" btree (a) INVALID
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_a_idx
+Partitioned index "global_temp_tests.tmp2_a_idx"
+ Column |  Type   | Key? | Definition 
+--------+---------+------+------------
+ a      | integer | yes  | a
+btree, for table "global_temp_tests.tmp2", invalid
+Number of partitions: 0
+
+CREATE INDEX tmp2_p1_a_idx ON tmp2_p1 (a);
+ALTER INDEX tmp2_a_idx ATTACH PARTITION tmp2_p1_a_idx;
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+  LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+    relname    | global_valid | local_valid 
+---------------+--------------+-------------
+ tmp2_a_idx    | t            | t
+ tmp2_p1_a_idx | t            | t
+(2 rows)
+
+\d tmp2
+Global temporary partitioned table "global_temp_tests.tmp2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition key: LIST (a)
+Indexes:
+    "tmp2_a_idx" btree (a)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_a_idx
+Partitioned index "global_temp_tests.tmp2_a_idx"
+ Column |  Type   | Key? | Definition 
+--------+---------+------+------------
+ a      | integer | yes  | a
+btree, for table "global_temp_tests.tmp2"
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_p1_a_idx
+Index "global_temp_tests.tmp2_p1_a_idx"
+ Column |  Type   | Key? | Definition 
+--------+---------+------+------------
+ a      | integer | yes  | a
+Partition of: tmp2_a_idx 
+btree, for table "global_temp_tests.tmp2_p1"
+
 DROP TABLE tmp2;
 -- Test ALTER TABLE with rewrite
 CREATE GLOBAL TEMP TABLE tmp2 (a int);
@@ -510,10 +613,12 @@ SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
  pg_temp_class_oid_index
  pg_temp_statistic
  pg_temp_statistic_relid_att_inh_index
+ pg_temp_index
+ pg_temp_index_indexrelid_index
  tmp1_c_seq
  tmp1
  tmp1_pkey
-(7 rows)
+(9 rows)
 
 SELECT * FROM tmp1;
  a |  b  | c 
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index bccae052a15..2b4af8452e6 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -287,6 +287,7 @@ NOTICE:  checking pg_propgraph_property {pgptypid} => pg_type {oid}
 NOTICE:  checking pg_propgraph_property {pgpcollation} => pg_collation {oid}
 NOTICE:  checking pg_temp_class {oid} => pg_class {oid}
 NOTICE:  checking pg_temp_class {reltablespace} => pg_tablespace {oid}
+NOTICE:  checking pg_temp_index {indexrelid} => pg_class {oid}
 NOTICE:  checking pg_temp_statistic {starelid} => pg_class {oid}
 NOTICE:  checking pg_temp_statistic {staop1} => pg_operator {oid}
 NOTICE:  checking pg_temp_statistic {staop2} => pg_operator {oid}
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 4272824e894..3f887d3d369 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -129,6 +129,40 @@ SET search_path = global_temp_tests;
 SELECT * FROM tmp2 ORDER BY a;
 DROP TABLE tmp2;
 
+-- Test partitioned index validity
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE INDEX tmp2_a_idx ON tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+  LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+\d tmp2
+\d tmp2_a_idx
+
+DROP INDEX tmp2_a_idx;
+CREATE INDEX tmp2_a_idx ON ONLY tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+  LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+\d tmp2
+\d tmp2_a_idx
+
+CREATE INDEX tmp2_p1_a_idx ON tmp2_p1 (a);
+ALTER INDEX tmp2_a_idx ATTACH PARTITION tmp2_p1_a_idx;
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+  LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+\d tmp2
+\d tmp2_a_idx
+\d tmp2_p1_a_idx
+DROP TABLE tmp2;
+
 -- Test ALTER TABLE with rewrite
 CREATE GLOBAL TEMP TABLE tmp2 (a int);
 INSERT INTO tmp2 VALUES (1);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6b38155a48a..e2e00c053dd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -957,6 +957,7 @@ FormData_pg_subscription
 FormData_pg_subscription_rel
 FormData_pg_tablespace
 FormData_pg_temp_class
+FormData_pg_temp_index
 FormData_pg_transform
 FormData_pg_trigger
 FormData_pg_ts_config
@@ -1023,6 +1024,7 @@ Form_pg_subscription
 Form_pg_subscription_rel
 Form_pg_tablespace
 Form_pg_temp_class
+Form_pg_temp_index
 Form_pg_transform
 Form_pg_trigger
 Form_pg_ts_config
-- 
2.51.0

