From 33f7af8180b551fa3717dd7f1ccca943b6d0403a 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 v2 9/9] 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           |  13 +-
 src/backend/catalog/Makefile                |   1 +
 src/backend/catalog/global_temp.c           |  37 +-
 src/backend/catalog/index.c                 |  48 +-
 src/backend/catalog/meson.build             |   1 +
 src/backend/catalog/pg_temp_index.c         | 490 ++++++++++++++++++++
 src/backend/commands/indexcmds.c            |  40 +-
 src/backend/commands/repack.c               |   4 +-
 src/backend/commands/tablecmds.c            |  39 +-
 src/backend/utils/cache/lsyscache.c         |   5 +-
 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_index.h         |  92 ++++
 src/test/isolation/expected/global-temp.out |  81 ++++
 src/test/isolation/specs/global-temp.spec   |   2 +
 src/test/regress/expected/global_temp.out   | 103 ++++
 src/test/regress/expected/oidjoins.out      |   1 +
 src/test/regress/sql/global_temp.sql        |  34 ++
 src/tools/pgindent/typedefs.list            |   2 +
 22 files changed, 1059 insertions(+), 67 deletions(-)
 create mode 100644 src/backend/catalog/pg_temp_index.c
 create mode 100644 src/include/catalog/pg_temp_index.h

diff --git a/contrib/tcn/tcn.c b/contrib/tcn/tcn.c
index 6b4bc7960d9..3d5c4deeb1e 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_index.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 21cb4af577d..0801c8eb028 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,8 +1150,9 @@ 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 inserts to pg_temp_class and pg_temp_index */
 		PreCCI_PgTempClass();
+		PreCCI_PgTempIndex();
 
 		currentCommandId += 1;
 		if (currentCommandId == InvalidCommandId)
@@ -2364,8 +2366,9 @@ CommitTransaction(void)
 	 */
 	PreCommit_GlobalTempRelation();
 
-	/* Flush out any pending inserts to pg_temp_class */
+	/* Flush out any pending inserts to pg_temp_class and pg_temp_index */
 	PreCommit_PgTempClass();
+	PreCommit_PgTempIndex();
 
 	/*
 	 * Synchronize files that are created and not WAL-logged during this
@@ -2639,8 +2642,9 @@ PrepareTransaction(void)
 	 */
 	PreCommit_GlobalTempRelation();
 
-	/* Flush out any pending inserts to pg_temp_class */
+	/* Flush out any pending inserts to pg_temp_class and pg_temp_index */
 	PreCommit_PgTempClass();
+	PreCommit_PgTempIndex();
 
 	/*
 	 * Synchronize files that are created and not WAL-logged during this
@@ -5199,8 +5203,9 @@ 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 inserts to pg_temp_class and pg_temp_index */
 	PreSubCommit_PgTempClass();
+	PreSubCommit_PgTempIndex();
 
 	/*
 	 * If this subxact has started any unfinished parallel operation, clean up
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index b13293a933e..e9497eb202d 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -47,6 +47,7 @@ OBJS = \
 	pg_subscription.o \
 	pg_tablespace.o \
 	pg_temp_class.o \
+	pg_temp_index.o \
 	pg_type.o \
 	storage.o \
 	toasting.o
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index f130f3d03ec..b1d525f52d7 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -62,6 +62,7 @@
 #include "access/xlogutils.h"
 #include "catalog/global_temp.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/storage.h"
 #include "commands/sequence.h"
 #include "lib/dshash.h"
@@ -71,6 +72,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
 #include "utils/tuplestore.h"
@@ -953,13 +955,32 @@ 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);
+
+		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);
 	}
 }
@@ -977,6 +998,7 @@ void
 GlobalTempRelationDropped(Oid relid)
 {
 	GtrUsageEntry *entry;
+	char		relkind;
 
 	/*
 	 * Mark the relation's usage as ending in the current subtransaction, if
@@ -991,8 +1013,13 @@ 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);
+
+		relkind = get_rel_relkind(relid);
+		if (relkind == RELKIND_INDEX ||
+			relkind == RELKIND_PARTITIONED_INDEX)
+			DeletePgTempIndexTuple(relid);
 	}
 }
 
@@ -1119,8 +1146,9 @@ AtEOXact_GlobalTempRelation(bool isCommit)
 		}
 	}
 
-	/* Perform any pg_temp_class processing */
+	/* Perform any pg_temp_class and pg_temp_index processing */
 	AtEOXact_PgTempClass(isCommit);
+	AtEOXact_PgTempIndex(isCommit);
 
 	/* Now we're out of the transaction and can clear the lists */
 	eoxact_storage_list_len = 0;
@@ -1191,8 +1219,9 @@ 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);
+	AtEOSubXact_PgTempIndex(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 5b2a72a2029..945196eaff6 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 hasindex,
 							   double reltuples);
@@ -573,7 +575,8 @@ UpdateIndexRelation(Oid indexoid,
 					bool isexclusion,
 					bool immediate,
 					bool isvalid,
-					bool isready)
+					bool isready,
+					char relpersistence)
 {
 	int2vector *indkey;
 	oidvector  *indcollation;
@@ -674,6 +677,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);
+	}
 }
 
 
@@ -1054,7 +1074,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
@@ -3562,6 +3583,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);
 
@@ -3921,18 +3945,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 ||
@@ -3943,9 +3975,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/meson.build b/src/backend/catalog/meson.build
index 5386d960b40..5819b10ff1f 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -34,6 +34,7 @@ backend_sources += files(
   'pg_subscription.c',
   'pg_tablespace.c',
   'pg_temp_class.c',
+  'pg_temp_index.c',
   'pg_type.c',
   'storage.c',
   'toasting.c',
diff --git a/src/backend/catalog/pg_temp_index.c b/src/backend/catalog/pg_temp_index.c
new file mode 100644
index 00000000000..71eb51f0d27
--- /dev/null
+++ b/src/backend/catalog/pg_temp_index.c
@@ -0,0 +1,490 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_index.c
+ *	  routines to support manipulation of the pg_temp_index relation
+ *
+ * The pg_temp_index system catalog table is a global temporary table that
+ * stores local overrides to the indisvalid field from the pg_index table
+ * for the duration of the current session.  Currently, this is only used
+ * for global temporary relations, though in the future, it might also be
+ * used for local temporary relations.
+ *
+ * Much of the code here mirrors similar code in pg_temp_class.c, except
+ * that we don't bother tracking whether or not pg_temp_index has been
+ * opened in this session, since it is only needed after pg_temp_class is
+ * used.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/catalog/pg_temp_index.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/xact.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_index.h"
+#include "catalog/pg_temp_index.h"
+#include "utils/hsearch.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* Cached copy of the pg_temp_index tuple descriptor */
+static TupleDesc pg_temp_index_tupdesc = NULL;
+
+/* Pending inserts to pg_temp_index */
+typedef struct PendingInsert
+{
+	Oid			relid;			/* lookup key: OID the tuple is for */
+	HeapTuple	tuple;			/* copy of tuple to be inserted */
+} PendingInsert;
+
+static HTAB *pending_inserts = NULL;
+static bool have_pending_inserts = false;
+
+/* Memory context for all tuples pending insert */
+static MemoryContext pending_inserts_tupctx = NULL;
+
+/* Transaction nesting level the pending inserts are for */
+static int	pending_inserts_nest_level = 1;
+
+/*
+ * init_pending_inserts_hashtable
+ *
+ *	Initialize the pending inserts hashtable, if not already done.
+ */
+static void
+init_pending_inserts_hashtable(void)
+{
+	if (pending_inserts == NULL)
+	{
+		HASHCTL		ctl;
+
+		/* Create the hash table */
+		ctl.keysize = sizeof(Oid);
+		ctl.entrysize = sizeof(PendingInsert);
+
+		pending_inserts = hash_create("Pending pg_temp_index inserts",
+									  128, &ctl, HASH_ELEM | HASH_BLOBS);
+
+		/* Create a separate memory context for all tuples in it */
+		pending_inserts_tupctx = AllocSetContextCreate(TopMemoryContext,
+													   "Pending pg_temp_index tuples",
+													   ALLOCSET_DEFAULT_SIZES);
+	}
+}
+
+/*
+ * 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_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_pg_temp_index_inserts
+ *
+ *	Flush any pending inserts to pg_temp_index.
+ */
+static void
+flush_pending_pg_temp_index_inserts(void)
+{
+	Relation	pg_temp_index;
+	CatalogIndexState indstate;
+	HASH_SEQ_STATUS status;
+	PendingInsert *entry;
+
+	pg_temp_index = table_open(TempIndexRelationId, RowExclusiveLock);
+
+	/* Flush all pending inserts */
+	indstate = CatalogOpenIndexes(pg_temp_index);
+	hash_seq_init(&status, pending_inserts);
+	while ((entry = hash_seq_search(&status)) != NULL)
+	{
+		CatalogTupleInsertWithInfo(pg_temp_index, entry->tuple, indstate);
+		hash_search(pending_inserts, &entry->relid, HASH_REMOVE, NULL);
+	}
+	CatalogCloseIndexes(indstate);
+	table_close(pg_temp_index, RowExclusiveLock);
+
+	/* Should be left with no pending inserts */
+	Assert(hash_get_num_entries(pending_inserts) == 0);
+	have_pending_inserts = false;
+
+	/* Free all memory allocated for tuples */
+	MemoryContextReset(pending_inserts_tupctx);
+}
+
+/*
+ * discard_pending_pg_temp_index_inserts
+ *
+ *	Discard any pending inserts to pg_temp_index.
+ */
+static void
+discard_pending_pg_temp_index_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;
+	have_pending_inserts = false;
+}
+
+/*
+ * 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 (have_pending_inserts)
+	{
+		entry = hash_search(pending_inserts, &indexrelid, HASH_FIND, NULL);
+		if (entry != NULL)
+			return heap_copytuple(entry->tuple);
+	}
+
+	/* Otherwise fetch a copy of the tuple from the system caches */
+	return SearchSysCacheCopy1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+}
+
+/*
+ * 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, or a new subtranction is started.  However,
+ *	the new tuple *is* visible to the other functions defined here.
+ */
+void
+InsertPgTempIndexTuple(Oid indexrelid, bool indisvalid)
+{
+	int			nest_level = GetCurrentTransactionNestLevel();
+	PendingInsert *entry;
+	bool		found;
+	MemoryContext oldcontext;
+
+	/*
+	 * If the transaction nesting level has increased, flush all previous
+	 * pending inserts, so that all new pending inserts are for the same
+	 * subtransaction.  The nesting level should never decrease without us
+	 * knowing about it via AtEOSubXact_PgTempIndex().
+	 */
+	Assert(nest_level >= pending_inserts_nest_level);
+	if (nest_level > pending_inserts_nest_level)
+	{
+		if (have_pending_inserts)
+			flush_pending_pg_temp_index_inserts();
+		pending_inserts_nest_level = nest_level;
+	}
+
+	/*
+	 * 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_hashtable();
+
+	entry = hash_search(pending_inserts, &indexrelid, HASH_ENTER, &found);
+	if (found)
+		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);
+	MemoryContextSwitchTo(oldcontext);
+
+	have_pending_inserts = true;
+}
+
+/*
+ * 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, just update that */
+	if (have_pending_inserts)
+	{
+		PendingInsert *entry;
+		Form_pg_temp_index old_form;
+		Form_pg_temp_index new_form;
+
+		entry = hash_search(pending_inserts, &indexrelid, HASH_FIND, NULL);
+		if (entry != NULL)
+		{
+			old_form = (Form_pg_temp_index) GETSTRUCT(entry->tuple);
+			new_form = (Form_pg_temp_index) GETSTRUCT(newtuple);
+			old_form->indisvalid = new_form->indisvalid;
+
+			return;
+		}
+	}
+
+	/* Otherwise, update pg_temp_index directly */
+	pg_temp_index = table_open(TempIndexRelationId, 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);
+}
+
+/*
+ * 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, just delete that */
+	if (have_pending_inserts)
+	{
+		PendingInsert *entry;
+
+		entry = hash_search(pending_inserts, &indexrelid, HASH_REMOVE, NULL);
+		if (entry != NULL)
+		{
+			heap_freetuple(entry->tuple);
+			return;
+		}
+	}
+
+	/* Otherwise, update pg_temp_index directly */
+	pg_temp_index = table_open(TempIndexRelationId, 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);
+}
+
+/*
+ * 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;
+}
+
+/*
+ * 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;
+}
+
+/*
+ * PreCCI_PgTempIndex
+ *
+ *	Pre-end-of-command processing; flush out any pending inserts.
+ */
+void
+PreCCI_PgTempIndex(void)
+{
+	if (have_pending_inserts)
+		flush_pending_pg_temp_index_inserts();
+}
+
+/*
+ * PreCommit_PgTempIndex
+ *
+ *	Pre-commit processing; flush out any pending inserts.
+ */
+void
+PreCommit_PgTempIndex(void)
+{
+	if (have_pending_inserts)
+		flush_pending_pg_temp_index_inserts();
+}
+
+/*
+ * PreSubCommit_PgTempIndex
+ *
+ *	Pre-subcommit processing; flush out any pending inserts.
+ */
+void
+PreSubCommit_PgTempIndex(void)
+{
+	if (have_pending_inserts)
+		flush_pending_pg_temp_index_inserts();
+}
+
+/*
+ * AtEOXact_PgTempIndex
+ *
+ *	Main-transaction commit or abort processing.
+ */
+void
+AtEOXact_PgTempIndex(bool isCommit)
+{
+	/* On rollback, discard any pending inserts */
+	if (!isCommit && have_pending_inserts)
+		discard_pending_pg_temp_index_inserts();
+
+	/*
+	 * Reset the pending inserts transaction nesting level so that inserts are
+	 * flushed if a new subtransaction is started, but not a new top-level
+	 * transaction.
+	 */
+	pending_inserts_nest_level = 1;
+}
+
+/*
+ * AtEOSubXact_PgTempIndex
+ *
+ *	Sub-transaction commit or abort processing.
+ */
+void
+AtEOSubXact_PgTempIndex(bool isCommit, SubTransactionId mySubid,
+						SubTransactionId parentSubid)
+{
+	/*
+	 * If pending inserts are for this transaction nesting level, and we're
+	 * rolling back, discard them.
+	 */
+	if (!isCommit && have_pending_inserts &&
+		pending_inserts_nest_level == GetCurrentTransactionNestLevel())
+	{
+		discard_pending_pg_temp_index_inserts();
+	}
+	pending_inserts_nest_level--;
+}
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 25d828f3a2b..29ae5d912ad 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -37,6 +37,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
@@ -259,7 +260,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);
@@ -272,7 +273,7 @@ CheckIndexCompatible(Oid oldId,
 		  heap_attisnull(tuple, Anum_pg_index_indexprs, NULL) &&
 		  indexForm->indisvalid))
 	{
-		ReleaseSysCache(tuple);
+		heap_freetuple(tuple);
 		return false;
 	}
 
@@ -289,7 +290,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;
@@ -1571,19 +1572,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 0356317a047..feb8790838e 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -52,6 +52,7 @@
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
@@ -866,8 +867,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 09a485e41d2..dbebae6cc9c 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"
@@ -1852,7 +1853,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);
@@ -1861,7 +1862,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)
@@ -13731,7 +13732,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);
@@ -13751,7 +13752,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 			*indexOid = indexoid;
 			break;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 	}
 
 	list_free(indexoidlist);
@@ -13789,7 +13790,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	*pk_has_without_overlaps = indexStruct->indisexclusion;
 
-	ReleaseSysCache(indexTuple);
+	heap_freetuple(indexTuple);
 
 	return i;
 }
@@ -13852,7 +13853,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);
@@ -13928,7 +13929,7 @@ transformFkeyCheckAttrs(Relation pkrel,
 			if (found)
 				*pk_has_without_overlaps = indexStruct->indisexclusion;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 		if (found)
 			break;
 	}
@@ -22186,14 +22187,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 */
@@ -22208,20 +22208,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..8bf0742496e 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -41,6 +41,7 @@
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/pg_temp_statistic.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
@@ -3965,13 +3966,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 dce3a0532dd..6823044eff7 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -62,6 +62,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"
@@ -1509,6 +1510,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
 	 */
@@ -2406,8 +2424,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));
@@ -2435,7 +2452,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 */
@@ -4965,6 +4982,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
@@ -4988,6 +5007,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
@@ -4999,7 +5033,7 @@ RelationGetIndexList(Relation relation)
 		 * partitioned tables.
 		 */
 		if (index->indisprimary &&
-			(index->indisvalid ||
+			(indisvalid ||
 			 relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
 		{
 			pkeyIndex = index->indexrelid;
@@ -5009,7 +5043,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 7483a1ab72b..4c7c54ad2ce 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2521,6 +2521,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 >= 190000)	/* FIXME: needs to be PG20 */
+			{
+				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, "));
@@ -2565,6 +2579,8 @@ describeOneTableDetails(const char *schemaname,
 			if (tableinfo.relkind == RELKIND_INDEX)
 				add_tablespace_footer(&cont, tableinfo.relkind,
 									  tableinfo.tablespace, true);
+
+			PQclear(temp_result);
 		}
 
 		PQclear(result);
@@ -2600,6 +2616,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 ("
@@ -2620,67 +2637,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 >= 190000)	/* FIXME: needs to be PG20 */
+					{
+						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_index.h b/src/include/catalog/pg_temp_index.h
new file mode 100644
index 00000000000..59533896a4e
--- /dev/null
+++ b/src/include/catalog/pg_temp_index.h
@@ -0,0 +1,92 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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;
+}
+
+
+extern HeapTuple GetPgTempIndexTuple(Oid indexrelid);
+
+extern void InsertPgTempIndexTuple(Oid indexrelid, bool indisvalid);
+
+extern void UpdatePgTempIndexTuple(Oid indexrelid, HeapTuple newtuple);
+
+extern void DeletePgTempIndexTuple(Oid indexrelid);
+
+extern HeapTuple GetPgIndexAndPgTempIndexTuples(Oid indexrelid,
+												HeapTuple *temp_tuple,
+												bool check_temp);
+
+extern HeapTuple GetEffectivePgIndexTuple(Oid indexrelid);
+
+extern void PreCCI_PgTempIndex(void);
+
+extern void PreCommit_PgTempIndex(void);
+
+extern void PreSubCommit_PgTempIndex(void);
+
+extern void AtEOXact_PgTempIndex(bool isCommit);
+
+extern void AtEOSubXact_PgTempIndex(bool isCommit, SubTransactionId mySubid,
+									SubTransactionId parentSubid);
+
+#endif							/* PG_TEMP_INDEX_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 2c901d437fe..5cdfe92707b 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -396,6 +396,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 b01fa917391..cf77e6624ed 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -80,6 +80,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
@@ -103,6 +104,7 @@ permutation create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 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 374fe83bcd1..6b5860379a7 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -186,6 +186,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);
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 5563d4f19c7..f49f76197df 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -102,6 +102,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 e56a1b5e505..cfb6e7dd35d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -954,6 +954,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
@@ -1020,6 +1021,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

