From 9b8782936a6b4666ba943996c5a47ff510bbfc73 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 v10 10/11] Add pg_temp_index global temporary catalog table.

This just has indexrelid and indisvalid columns, allowing the valid
state of an index to be 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 +-
 doc/src/sgml/catalogs.sgml                  |  86 ++++++++
 src/backend/catalog/Makefile                |   1 +
 src/backend/catalog/genbki.pl               |   1 +
 src/backend/catalog/global_temp.c           |  36 +++-
 src/backend/catalog/index.c                 |  48 ++++-
 src/backend/catalog/meson.build             |   1 +
 src/backend/catalog/pg_temp_index.c         | 213 ++++++++++++++++++++
 src/backend/commands/indexcmds.c            |  40 +++-
 src/backend/commands/repack.c               |  12 +-
 src/backend/commands/tablecmds.c            |  44 ++--
 src/backend/utils/cache/gtcatcache.c        |  12 ++
 src/backend/utils/cache/lsyscache.c         |   5 +-
 src/backend/utils/cache/relcache.c          |  44 +++-
 src/bin/psql/describe.c                     |  70 +++++--
 src/include/catalog/Makefile                |   1 +
 src/include/catalog/meson.build             |   1 +
 src/include/catalog/pg_temp_index.h         |  76 +++++++
 src/include/utils/gtcatcache.h              |   3 +-
 src/test/isolation/expected/global-temp.out | 108 +++++++++-
 src/test/isolation/specs/global-temp.spec   |  12 +-
 src/test/regress/expected/global_temp.out   | 115 ++++++++++-
 src/test/regress/expected/oidjoins.out      |   1 +
 src/test/regress/sql/global_temp.sql        |  34 ++++
 src/tools/pgindent/typedefs.list            |   2 +
 25 files changed, 905 insertions(+), 68 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/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1ff0ad02265..73b4c0d9592 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -355,6 +355,11 @@
       <entry>global temporary relations used in the current session</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-temp-index"><structname>pg_temp_index</structname></link></entry>
+      <entry>additional index information for indexes on global temporary tables</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link></entry>
       <entry>planner statistics for global temporary relations</entry>
@@ -4614,6 +4619,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        <link linkend="sql-insert"><command>INSERT</command></link>/<link linkend="sql-update"><command>UPDATE</command></link> operations, but it cannot safely
        be used for queries. If it is unique, the uniqueness property is not
        guaranteed true either.
+      </para>
+      <para>
+       For an index on a global temporary table, the value from
+       <link linkend="catalog-pg-temp-index"><structname>pg_temp_index</structname></link>.<structfield>indisvalid</structfield>,
+       if any, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -9284,6 +9294,82 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
  </sect1>
 
 
+ <sect1 id="catalog-pg-temp-index">
+  <title><structname>pg_temp_index</structname></title>
+
+  <indexterm zone="catalog-pg-temp-index">
+   <primary>pg_temp_index</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_temp_index</structname> is a global temporary
+   table that stores information about indexes on global temporary tables in
+   use in the current session.
+  </para>
+
+  <para>
+   The attributes of <structname>pg_temp_index</structname> are a subset of
+   the attributes of
+   <link linkend="catalog-pg-index"><structname>pg_index</structname></link>,
+   used to store local overrides to the values from
+   <structname>pg_index</structname> for each session.  Currently, the only
+   attribute is <structfield>indisvalid</structfield>, used to indicate
+   whether an index is valid in the current session &mdash; a new index
+   created on a global temporary table will be marked invalid in all other
+   currently active sessions that have already added data to the table (see
+   <link linkend="sql-createindex"><command>CREATE INDEX</command></link>).
+  </para>
+
+  <table>
+   <title><structname>pg_temp_index</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>indexrelid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the <link linkend="catalog-pg-class"><structname>pg_class</structname></link> entry for this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>indisvalid</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the index is currently valid for queries in the current session.
+       False means the index is possibly incomplete: it must still be modified by
+       <link linkend="sql-insert"><command>INSERT</command></link>/<link linkend="sql-update"><command>UPDATE</command></link> operations, but it cannot safely
+       be used for queries. If it is unique, the uniqueness property is not
+       guaranteed true either.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <note>
+   <para>
+    Tuples are only added to <structname>pg_temp_index</structname> for indexes
+    on global temporary tables that have been used in the current session.
+   </para>
+  </note>
+ </sect1>
+
+
  <sect1 id="catalog-pg-temp-statistic">
   <title><structname>pg_temp_statistic</structname></title>
 
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/genbki.pl b/src/backend/catalog/genbki.pl
index fded3bfcf6c..9a8ce3d2c00 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -801,6 +801,7 @@ print $syscache_ids_fh "#ifndef SYSCACHE_IDS_H
 #define SYSCACHE_IDS_H
 
 #include \"catalog/pg_temp_class_d.h\"
+#include \"catalog/pg_temp_index_d.h\"
 #include \"catalog/pg_temp_statistic_d.h\"
 #include \"catalog/pg_temp_statistic_ext_data_d.h\"
 
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 3a6e58e14ef..fe1a352fb2c 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -62,6 +62,7 @@
 #include "catalog/global_temp.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "catalog/storage.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
@@ -123,6 +124,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;
@@ -583,6 +585,9 @@ gtr_record_usage(Oid relid, char relkind)
 	}
 	local_entry->stopped_subid = InvalidSubTransactionId;
 
+	/* Remember the relation's relkind */
+	local_entry->relkind = relkind;
+
 	/* Add/update shared usage entry */
 	key.dbid = MyDatabaseId;
 	key.relid = relid;
@@ -968,6 +973,24 @@ TrackGlobalTempRelation(Relation relation)
 		gtr_record_usage(relation->rd_id, relation->rd_rel->relkind);
 		InsertPgTempClassTuple(relation);
 
+		/* For an index, also insert a pg_temp_index tuple */
+		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);
+		}
+
 		/*
 		 * If this backend's tempfrozenxid and tempminmxid haven't been set
 		 * yet (this is the first global temporary relation accessed in this
@@ -1004,9 +1027,13 @@ ForgetGlobalTempRelation(Oid relid)
 	entry->stopped_subid = GetCurrentSubTransactionId();
 	EOXactUsageListAdd(relid);
 
-	/* Delete its pg_temp_class tuple */
+	/* Delete its pg_temp_class and pg_temp_index tuples */
 	DeletePgTempClassTuple(relid);
 
+	if (entry->relkind == RELKIND_INDEX ||
+		entry->relkind == RELKIND_PARTITIONED_INDEX)
+		DeletePgTempIndexTuple(relid);
+
 	/* Update this backend's tempfrozenxid and tempminmxid */
 	UpdateTempFrozenXids();
 }
@@ -1201,6 +1228,13 @@ ProcessInvalidatedGlobalTempRelations(void)
 				tuples_deleted = true;
 			}
 
+			/* For an index, delete its pg_temp_index tuple, if it has one */
+			if (PgTempIndexTupleExists(relid))
+			{
+				DeletePgTempIndexTuple(relid);
+				tuples_deleted = true;
+			}
+
 			/* Delete any per-column statistics from pg_temp_statistic */
 			ScanKeyInit(&key[0],
 						Anum_pg_temp_statistic_starelid,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 127fafd81f5..e62b3218a43 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, TrackGlobalTempRelation()
+	 * 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);
+	}
 }
 
 
@@ -1063,7 +1083,8 @@ index_create(Relation heapRelation,
 						(constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0 &&
 						(flags & INDEX_CREATE_DEFERRABLE) == 0,
 						!concurrent && !invalid,
-						!concurrent);
+						!concurrent,
+						relpersistence);
 
 	/*
 	 * Register relcache invalidation on the indexes' heap relation, to
@@ -3609,6 +3630,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);
 
@@ -3968,18 +3992,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 ||
@@ -3990,9 +4022,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..c623bd7e591
--- /dev/null
+++ b/src/backend/catalog/pg_temp_index.c
@@ -0,0 +1,213 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 --- see
+ * the comments there for more detail.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *   src/backend/catalog/pg_temp_index.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_temp_index.h"
+#include "utils/gtcatcache.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+
+/* Cached copy of the pg_temp_index tuple descriptor */
+static TupleDesc pg_temp_index_tupdesc = NULL;
+
+/*
+ * 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;
+}
+
+/*
+ * PgTempIndexTupleExists
+ *
+ *	Test if a pg_temp_index tuple for a global temporary index exists.
+ */
+bool
+PgTempIndexTupleExists(Oid indexrelid)
+{
+	return GTCatCacheTupleExists(PG_TEMP_INDEX, indexrelid);
+}
+
+/*
+ * 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)
+{
+	return GTCatCacheSearch(PG_TEMP_INDEX, 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.  However, the new tuple *is* visible to all
+ *	the functions defined here.
+ */
+void
+InsertPgTempIndexTuple(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);
+
+	GTCatCacheTupleInsert(PG_TEMP_INDEX, indexrelid, RELKIND_INDEX,
+						  get_pg_temp_index_tupdesc(), values, nulls);
+}
+
+/*
+ * UpdatePgTempIndexTuple
+ *
+ *	Update the pg_temp_index tuple for a global temporary index relation.
+ */
+void
+UpdatePgTempIndexTuple(Oid indexrelid, HeapTuple newtuple)
+{
+	GTCatCacheTupleUpdate(PG_TEMP_INDEX, indexrelid, newtuple);
+}
+
+/*
+ * DeletePgTempIndexTuple
+ *
+ *	Delete the pg_temp_index tuple for a global temporary index relation.
+ */
+void
+DeletePgTempIndexTuple(Oid indexrelid)
+{
+	GTCatCacheTupleDelete(PG_TEMP_INDEX, indexrelid);
+}
+
+/*
+ * 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;
+}
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index aa38c048630..41b140a6d6d 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -38,6 +38,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"
@@ -270,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);
@@ -283,7 +284,7 @@ CheckIndexCompatible(Oid oldId,
 		  heap_attisnull(tuple, Anum_pg_index_indexprs, NULL) &&
 		  indexForm->indisvalid))
 	{
-		ReleaseSysCache(tuple);
+		heap_freetuple(tuple);
 		return false;
 	}
 
@@ -300,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;
@@ -1598,19 +1599,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 3eb8a8c090a..090aab7741b 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -53,6 +53,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"
@@ -836,12 +837,15 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 	foreach(index, RelationGetIndexList(rel))
 	{
 		Oid			thisIndexOid = lfirst_oid(index);
+		HeapTuple	temp_indexTuple;
+		Form_pg_temp_index temp_indexForm;
 
-		indexTuple = SearchSysCacheCopy1(INDEXRELID,
-										 ObjectIdGetDatum(thisIndexOid));
+		indexTuple = GetPgIndexAndPgTempIndexTuples(thisIndexOid,
+													&temp_indexTuple, false);
 		if (!HeapTupleIsValid(indexTuple))
 			elog(ERROR, "cache lookup failed for index %u", thisIndexOid);
 		indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+		temp_indexForm = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_indexTuple);
 
 		/*
 		 * Unset the bit if set.  We know it's wrong because we checked this
@@ -855,7 +859,7 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 		else if (thisIndexOid == indexOid)
 		{
 			/* this was checked earlier, but let's be real sure */
-			if (!indexForm->indisvalid)
+			if (!GetEffective_indisvalid(indexForm, temp_indexForm))
 				elog(ERROR, "cannot cluster on invalid index %u", indexOid);
 			indexForm->indisclustered = true;
 			CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
@@ -865,6 +869,8 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 									 InvalidOid, is_internal);
 
 		heap_freetuple(indexTuple);
+		if (HeapTupleIsValid(temp_indexTuple))
+			heap_freetuple(temp_indexTuple);
 	}
 
 	table_close(pg_index, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb4132ee4ed..120cb37ef33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -54,6 +54,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"
@@ -1838,7 +1839,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);
@@ -1847,7 +1848,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)
@@ -9883,7 +9884,10 @@ ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
 	/*
 	 * Don't allow constraints to be added to global temporary tables that are
 	 * being used by other sessions, because we have no way to scan the local
-	 * storage of another backend to validate the constraint.
+	 * storage of another backend to validate the constraint.  Note, however,
+	 * that we do allow unique indexes to be created directly, even if another
+	 * session is using the table, by marking the index as invalid in the
+	 * other session.
 	 *
 	 * XXX: Do we actually need to do this? Adding a PRIMARY KEY is already
 	 * blocked for an in-use GTT by the check in ATRewriteTables(), and maybe
@@ -14085,7 +14089,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);
@@ -14105,7 +14109,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 			*indexOid = indexoid;
 			break;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 	}
 
 	list_free(indexoidlist);
@@ -14143,7 +14147,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	*pk_has_without_overlaps = indexStruct->indisexclusion;
 
-	ReleaseSysCache(indexTuple);
+	heap_freetuple(indexTuple);
 
 	return i;
 }
@@ -14206,7 +14210,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);
@@ -14282,7 +14286,7 @@ transformFkeyCheckAttrs(Relation pkrel,
 			if (found)
 				*pk_has_without_overlaps = indexStruct->indisexclusion;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 		if (found)
 			break;
 	}
@@ -22751,14 +22755,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 */
@@ -22773,20 +22776,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/gtcatcache.c b/src/backend/utils/cache/gtcatcache.c
index 371ae33faa3..89f8be08543 100644
--- a/src/backend/utils/cache/gtcatcache.c
+++ b/src/backend/utils/cache/gtcatcache.c
@@ -52,6 +52,7 @@
 #include "access/xact.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
 #include "utils/fmgroids.h"
 #include "utils/gtcatcache.h"
 #include "utils/hsearch.h"
@@ -143,6 +144,17 @@ static GTCatCache gt_cat_cache[NUM_GT_CAT_CACHES] = {
 		.eoxact_list_len = 0,
 		.eoxact_list_overflowed = false,
 	},
+	/* PG_TEMP_INDEX */
+	{
+		.name = "pg_temp_index cache",
+		.catalog_relid = TempIndexRelationId,
+		.index_relid = TempIndexRelidIndexId,
+		.key_attno = Anum_pg_temp_index_indexrelid,
+		.cacheid = TEMPINDEXRELID,
+		.hashtable = NULL,
+		.eoxact_list_len = 0,
+		.eoxact_list_overflowed = false,
+	},
 };
 
 /*
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index dd7b04b5e3d..e1330c31118 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"
@@ -3973,13 +3974,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 ea23e88e97e..091a3a59e16 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_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/schemapg.h"
@@ -1507,6 +1508,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
 	 */
@@ -2405,8 +2423,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));
@@ -2434,7 +2451,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 */
@@ -4997,6 +5014,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
@@ -5020,6 +5039,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
@@ -5031,7 +5065,7 @@ RelationGetIndexList(Relation relation)
 		 * partitioned tables.
 		 */
 		if (index->indisprimary &&
-			(index->indisvalid ||
+			(indisvalid ||
 			 relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
 		{
 			pkeyIndex = index->indexrelid;
@@ -5041,7 +5075,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 b32e61301ac..4e24e3ed971 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2353,8 +2353,15 @@ describeOneTableDetails(const char *schemaname,
 
 		printfPQExpBuffer(&buf, "/* %s */\n", _("Get index details"));
 		appendPQExpBufferStr(&buf,
-							 "SELECT i.indisunique, i.indisprimary, i.indisclustered, "
-							 "i.indisvalid,\n"
+							 "SELECT i.indisunique, i.indisprimary, i.indisclustered, ");
+
+		if (pset.sversion >= 200000)
+			appendPQExpBufferStr(&buf,
+								 "COALESCE(ti.indisvalid, i.indisvalid),\n");
+		else
+			appendPQExpBufferStr(&buf, "i.indisvalid,\n");
+
+		appendPQExpBufferStr(&buf,
 							 "  (NOT i.indimmediate) AND "
 							 "EXISTS (SELECT 1 FROM pg_catalog.pg_constraint "
 							 "WHERE conrelid = i.indrelid AND "
@@ -2379,12 +2386,21 @@ describeOneTableDetails(const char *schemaname,
 		else
 			appendPQExpBufferStr(&buf, "false AS indnullsnotdistinct,\n");
 
-		appendPQExpBuffer(&buf, "  a.amname, c2.relname, "
-						  "pg_catalog.pg_get_expr(i.indpred, i.indrelid, true)\n"
-						  "FROM pg_catalog.pg_index i, pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_am a\n"
-						  "WHERE i.indexrelid = c.oid AND c.oid = '%s' AND c.relam = a.oid\n"
-						  "AND i.indrelid = c2.oid;",
-						  oid);
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf, "  a.amname, c2.relname, "
+							  "pg_catalog.pg_get_expr(i.indpred, i.indrelid, true)\n"
+							  "FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_am a, pg_catalog.pg_index i\n"
+							  "  LEFT JOIN pg_catalog.pg_temp_index ti ON ti.indexrelid = i.indexrelid\n"
+							  "WHERE i.indexrelid = c.oid AND c.oid = '%s' AND c.relam = a.oid\n"
+							  "AND i.indrelid = c2.oid;",
+							  oid);
+		else
+			appendPQExpBuffer(&buf, "  a.amname, c2.relname, "
+							  "pg_catalog.pg_get_expr(i.indpred, i.indrelid, true)\n"
+							  "FROM pg_catalog.pg_index i, pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_am a\n"
+							  "WHERE i.indexrelid = c.oid AND c.oid = '%s' AND c.relam = a.oid\n"
+							  "AND i.indrelid = c2.oid;",
+							  oid);
 
 		result = PSQLexec(buf.data);
 		if (!result)
@@ -2473,7 +2489,13 @@ describeOneTableDetails(const char *schemaname,
 			printfPQExpBuffer(&buf, "/* %s */\n", _("Get indexes for this table"));
 			appendPQExpBufferStr(&buf,
 								 "SELECT c2.relname, i.indisprimary, i.indisunique, "
-								 "i.indisclustered, i.indisvalid, "
+								 "i.indisclustered, ");
+			if (pset.sversion >= 200000)
+				appendPQExpBufferStr(&buf,
+									 "COALESCE(ti.indisvalid, i.indisvalid), ");
+			else
+				appendPQExpBufferStr(&buf, "i.indisvalid, ");
+			appendPQExpBufferStr(&buf,
 								 "pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),\n  "
 								 "pg_catalog.pg_get_constraintdef(con.oid, true), "
 								 "contype, condeferrable, condeferred");
@@ -2483,15 +2505,27 @@ describeOneTableDetails(const char *schemaname,
 				appendPQExpBufferStr(&buf, ", con.conperiod");
 			else
 				appendPQExpBufferStr(&buf, ", false AS conperiod");
-			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 ("
-							  CppAsString2(CONSTRAINT_PRIMARY) ","
-							  CppAsString2(CONSTRAINT_UNIQUE) ","
-							  CppAsString2(CONSTRAINT_EXCLUSION) "))\n"
-							  "WHERE c.oid = '%s' AND c.oid = i.indrelid AND i.indexrelid = c2.oid\n"
-							  "ORDER BY i.indisprimary DESC, c2.relname;",
-							  oid);
+			if (pset.sversion >= 200000)
+				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 ("
+								  CppAsString2(CONSTRAINT_PRIMARY) ","
+								  CppAsString2(CONSTRAINT_UNIQUE) ","
+								  CppAsString2(CONSTRAINT_EXCLUSION) "))\n"
+								  "  LEFT JOIN pg_catalog.pg_temp_index ti ON ti.indexrelid = i.indexrelid\n"
+								  "WHERE c.oid = '%s' AND c.oid = i.indrelid AND i.indexrelid = c2.oid\n"
+								  "ORDER BY i.indisprimary DESC, c2.relname;",
+								  oid);
+			else
+				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 ("
+								  CppAsString2(CONSTRAINT_PRIMARY) ","
+								  CppAsString2(CONSTRAINT_UNIQUE) ","
+								  CppAsString2(CONSTRAINT_EXCLUSION) "))\n"
+								  "WHERE c.oid = '%s' AND c.oid = i.indrelid AND i.indexrelid = c2.oid\n"
+								  "ORDER BY i.indisprimary DESC, c2.relname;",
+								  oid);
 			result = PSQLexec(buf.data);
 			if (!result)
 				goto error_return;
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..313536ccea3
--- /dev/null
+++ b/src/include/catalog/pg_temp_index.h
@@ -0,0 +1,76 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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/htup.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 bool PgTempIndexTupleExists(Oid relid);
+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);
+
+#endif							/* PG_TEMP_INDEX_H */
diff --git a/src/include/utils/gtcatcache.h b/src/include/utils/gtcatcache.h
index 0879e37b469..edede00453b 100644
--- a/src/include/utils/gtcatcache.h
+++ b/src/include/utils/gtcatcache.h
@@ -21,9 +21,10 @@
 typedef enum GTCatCacheIdentifier
 {
 	PG_TEMP_CLASS,
+	PG_TEMP_INDEX,
 } GTCatCacheIdentifier;
 
-#define NUM_GT_CAT_CACHES	((int) PG_TEMP_CLASS + 1)
+#define NUM_GT_CAT_CACHES	((int) PG_TEMP_INDEX + 1)
 
 extern bool GTCatCacheTupleExists(GTCatCacheIdentifier cacheId, Oid relid);
 extern HeapTuple GTCatCacheSearch(GTCatCacheIdentifier cacheId, Oid relid);
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index b021c2f26e7..ac88add9e00 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -236,7 +236,7 @@ key|pg_typeof|val
 
 step drop1: DROP TABLE tmp2;
 
-starting permutation: create1 ins1_2 ins2_2 alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h uniq_idx1 seltype1 seltype2 drop1
+starting permutation: create1 ins1_2 ins2_2 alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h uniq_idx1 seltype1 seltype2 idx_valid1 idx_valid2 uniq_reidx2 idx_valid2 drop1
 step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
 step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
 step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
@@ -269,6 +269,31 @@ key|pg_typeof|val
   1|integer  |s2 
 (1 row)
 
+step idx_valid1: 
+  SELECT indisvalid FROM pg_temp_index WHERE indexrelid = 'tmp2_un'::regclass;
+
+indisvalid
+----------
+t         
+(1 row)
+
+step idx_valid2: 
+  SELECT indisvalid FROM pg_temp_index WHERE indexrelid = 'tmp2_un'::regclass;
+
+indisvalid
+----------
+f         
+(1 row)
+
+step uniq_reidx2: REINDEX INDEX tmp2_un;
+step idx_valid2: 
+  SELECT indisvalid FROM pg_temp_index WHERE indexrelid = 'tmp2_un'::regclass;
+
+indisvalid
+----------
+t         
+(1 row)
+
 step drop1: DROP TABLE tmp2;
 
 starting permutation: create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1
@@ -481,6 +506,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 8123feba835..fe392c181e7 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -47,6 +47,9 @@ step cat1 {
 step r1 { ROLLBACK; }
 step sp1 { SAVEPOINT sp; }
 step rsp1 { ROLLBACK TO SAVEPOINT sp; }
+step idx_valid1 {
+  SELECT indisvalid FROM pg_temp_index WHERE indexrelid = 'tmp2_un'::regclass;
+}
 step drop1 { DROP TABLE tmp2; }
 step c1 { COMMIT; }
 step prep1 { PREPARE TRANSACTION 'tx'; }
@@ -87,6 +90,10 @@ step rsp2 { ROLLBACK TO SAVEPOINT sp; }
 step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
 step sel2_2 { SELECT * FROM tmp2; }
 step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step idx_valid2 {
+  SELECT indisvalid FROM pg_temp_index WHERE indexrelid = 'tmp2_un'::regclass;
+}
+step uniq_reidx2 { REINDEX INDEX tmp2_un; }
 step drop2 { DROP TABLE tmp2; }
 step sel2_idx {
   SET enable_seqscan = off;
@@ -105,6 +112,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
@@ -126,7 +134,8 @@ permutation create1 ins1_2
             ins2_2 seltype1 seltype2 drop1
 permutation create1 ins1_2 ins2_2
             alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h
-            uniq_idx1 seltype1 seltype2 drop1
+            uniq_idx1 seltype1 seltype2
+            idx_valid1 idx_valid2 uniq_reidx2 idx_valid2 drop1
 
 # Test concurrent ON COMMIT DELETE ROWS
 permutation create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1
@@ -143,6 +152,7 @@ permutation create1 b1 ins1_2 drop2 prep1
 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 849d20e56c1..26f6bc5b52d 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -94,7 +94,9 @@ 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
-(4 rows)
+ pg_temp_index
+ pg_temp_index_indexrelid_index
+(6 rows)
 
 SELECT * FROM tmp1;
  a | b | c 
@@ -108,9 +110,11 @@ 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
  tmp1_pkey
-(6 rows)
+(8 rows)
 
 -- Test pg_relation_filenode() matches global relfilenode
 SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
@@ -255,6 +259,109 @@ SELECT tableoid::regclass, * FROM tmp2 ORDER BY a;
 (0 rows)
 
 DROP TABLE tmp2, perm;
+-- 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);
 INSERT INTO tmp2 VALUES (1);
@@ -537,10 +644,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 93226f06f6e..627db42040a 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -139,6 +139,40 @@ SET search_path = global_temp_tests;
 SELECT tableoid::regclass, * FROM tmp2 ORDER BY a;
 DROP TABLE tmp2, perm;
 
+-- 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 225a1132bbd..34a88b68bb8 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.43.0

