From 55eacc4c828bd8784f1692bde7d8917ca6048fc7 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Tue, 9 Jun 2026 13:50:47 +0100
Subject: [PATCH v11 3/9] Add support for indexes on global temporary tables.

Indexes on global temporary tables are themselves global temporary.
When an index is defined, it is fully populated in the session that
defined it, but in any other active sessions an empty index is built,
and if those sessions already have data in their copies of the table,
the index is marked invalid, and is only usable if a REINDEX is
executed in those sessions.

All types of index are supported, including unique indexes, which
enables foreign keys between global temporary tables.

As with indexes on local temporary tables, concurrent index
build/reindex/drop is not supported.
---
 contrib/bloom/blinsert.c                      |   6 +-
 contrib/bloom/bloom.h                         |   2 +-
 contrib/tcn/tcn.c                             |   7 +-
 doc/src/sgml/catalogs.sgml                    |   5 +
 doc/src/sgml/func/func-info.sgml              |  18 +
 doc/src/sgml/indexam.sgml                     |  17 +-
 doc/src/sgml/ref/create_index.sgml            |  16 +
 src/backend/access/brin/brin.c                |   4 +-
 src/backend/access/gin/gininsert.c            |   8 +-
 src/backend/access/gist/gist.c                |   6 +-
 src/backend/access/hash/hash.c                |  12 +-
 src/backend/access/nbtree/nbtree.c            |   6 +-
 src/backend/access/spgist/spginsert.c         |   6 +-
 src/backend/access/transam/xloginsert.c       |   3 +-
 src/backend/catalog/global_temp.c             | 115 +++++-
 src/backend/catalog/index.c                   |  46 ++-
 src/backend/commands/indexcmds.c              |  46 ++-
 src/backend/commands/repack.c                 |   9 +-
 src/backend/commands/tablecmds.c              |  60 ++-
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/parser/parse_utilcmd.c            |   9 +-
 src/backend/utils/cache/lsyscache.c           |  15 +-
 src/backend/utils/cache/relcache.c            |  28 +-
 src/bin/psql/describe.c                       |  25 +-
 src/bin/scripts/reindexdb.c                   |   4 +
 src/include/access/amapi.h                    |   4 +-
 src/include/access/brin_internal.h            |   2 +-
 src/include/access/gin_private.h              |   2 +-
 src/include/access/gist_private.h             |   2 +-
 src/include/access/hash.h                     |   2 +-
 src/include/access/nbtree.h                   |   2 +-
 src/include/access/spgist.h                   |   2 +-
 src/include/catalog/global_temp.h             |  18 +-
 src/include/catalog/pg_proc.dat               |   3 +
 src/include/nodes/parsenodes.h                |   3 +-
 src/include/utils/lsyscache.h                 |   1 +
 src/test/isolation/expected/global-temp.out   | 288 ++++++++++++++-
 src/test/isolation/specs/global-temp.spec     |  54 ++-
 .../modules/dummy_index_am/dummy_index_am.c   |   4 +-
 src/test/regress/expected/global_temp.out     | 344 +++++++++++++++---
 src/test/regress/sql/global_temp.sql          | 120 +++++-
 41 files changed, 1154 insertions(+), 171 deletions(-)

diff --git a/contrib/bloom/blinsert.c b/contrib/bloom/blinsert.c
index df24856d9ae..804c77ae768 100644
--- a/contrib/bloom/blinsert.c
+++ b/contrib/bloom/blinsert.c
@@ -159,13 +159,13 @@ blbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 }
 
 /*
- * Build an empty bloom index in the initialization fork.
+ * Build an empty bloom index in the specified fork.
  */
 void
-blbuildempty(Relation index)
+blbuildempty(Relation index, ForkNumber forknum)
 {
 	/* Initialize the meta page */
-	BloomInitMetapage(index, INIT_FORKNUM);
+	BloomInitMetapage(index, forknum);
 }
 
 /*
diff --git a/contrib/bloom/bloom.h b/contrib/bloom/bloom.h
index 9a4125bdfb1..4f00584006b 100644
--- a/contrib/bloom/bloom.h
+++ b/contrib/bloom/bloom.h
@@ -197,7 +197,7 @@ extern void blrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 extern void blendscan(IndexScanDesc scan);
 extern IndexBuildResult *blbuild(Relation heap, Relation index,
 								 struct IndexInfo *indexInfo);
-extern void blbuildempty(Relation index);
+extern void blbuildempty(Relation index, ForkNumber forknum);
 extern IndexBulkDeleteResult *blbulkdelete(IndexVacuumInfo *info,
 										   IndexBulkDeleteResult *stats, IndexBulkDeleteCallback callback,
 										   void *callback_state);
diff --git a/contrib/tcn/tcn.c b/contrib/tcn/tcn.c
index 6b4bc7960d9..13b1abe1bc6 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/global_temp.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 95e8d49e7a6..6f102a84d15 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -4547,6 +4547,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="pg-gtr-index-is-valid"><function>pg_gtr_index_is_valid()</function></link>,
+       if non-null, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index fe7677a03ec..9e30b03ca23 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -346,6 +346,24 @@
        </para></entry>
       </row>
 
+      <row>
+       <entry id="pg-gtr-index-is-valid" role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_gtr_index_is_valid</primary>
+        </indexterm>
+        <function>pg_gtr_index_is_valid</function> ( <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Returns true if an index on a global temporary relation is valid in
+        the current session, and false if it is invalid
+        (<xref linkend="sql-reindex"/> needs to be run to make it usable).
+        If the input is not the OID of a global temporary index, or that index
+        has not yet been opened in the current session, <literal>NULL</literal>
+        is returned.
+       </para></entry>
+      </row>
+
       <row>
        <entry id="pg-gtr-info" role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index f48da318530..6649bdd6cd1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -312,15 +312,20 @@ ambuild (Relation heapRelation,
    this flag set to <literal>false</literal>.
   </para>
 
-  <para>
+  <para id="indexam-ambuildempty">
 <programlisting>
 void
-ambuildempty (Relation indexRelation);
+ambuildempty (Relation indexRelation,
+              ForkNumber forknum);
 </programlisting>
-   Build an empty index, and write it to the initialization fork (<symbol>INIT_FORKNUM</symbol>)
-   of the given relation.  This method is called only for unlogged indexes; the
-   empty index written to the initialization fork will be copied over the main
-   relation fork on each server restart.
+   Build an empty index, and write it to the specified fork of the given
+   relation.  This method is called for global temporary indexes and for
+   unlogged indexes.  For global temporary indexes, the empty index is written
+   to the main relation fork (<symbol>MAIN_FORKNUM</symbol>) in session-local
+   storage the first time the index is accessed in the session.  For unlogged
+   indexes, the empty index is written to the initialization fork
+   (<symbol>INIT_FORKNUM</symbol>) and then copied over the main relation
+   fork on each server restart.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index bb7505d171b..2dac47c0499 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -770,6 +770,22 @@ Indexes:
    specified.
   </para>
 
+  <para>
+   When <literal>CREATE INDEX</literal> is invoked on a global temporary
+   table, the index is initially only populated in the session that executed
+   <literal>CREATE INDEX</literal>.  Other active sessions will see the
+   index definition and automatically build an empty index using the
+   <link linkend="indexam-ambuildempty"><literal>ambuildempty</literal></link>
+   index access method, but if another active session already has data in the
+   table, the index will be marked invalid in that session, and will not be
+   usable unless <link linkend="sql-reindex"><command>REINDEX</command></link>
+   is used to rebuild it in that session.  Other active sessions that have not
+   yet inserted data into the table will be able to use the index normally;
+   each session's copy of the index will be populated as data is added to that
+   session's copy of the table.  Likewise for any future sessions started
+   after the index is defined.
+  </para>
+
   <para>
    For index methods that support ordered scans (currently, only B-tree),
    the optional clauses <literal>ASC</literal>, <literal>DESC</literal>, <literal>NULLS
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 5059da8dce7..4705266d830 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -1276,12 +1276,12 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 }
 
 void
-brinbuildempty(Relation index)
+brinbuildempty(Relation index, ForkNumber forknum)
 {
 	Buffer		metabuf;
 
 	/* An empty BRIN index has a metapage only. */
-	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	metabuf = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer. */
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index aaef7020981..147adfb0965 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -806,18 +806,18 @@ ginbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 }
 
 /*
- *	ginbuildempty() -- build an empty gin index in the initialization fork
+ *	ginbuildempty() -- build an empty gin index in the specified fork
  */
 void
-ginbuildempty(Relation index)
+ginbuildempty(Relation index, ForkNumber forknum)
 {
 	Buffer		RootBuffer,
 				MetaBuffer;
 
 	/* An empty GIN index has two pages. */
-	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	MetaBuffer = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
-	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	RootBuffer = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..9eae449786b 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -134,15 +134,15 @@ createTempGistContext(void)
 }
 
 /*
- *	gistbuildempty() -- build an empty gist index in the initialization fork
+ *	gistbuildempty() -- build an empty gist index in the specified fork
  */
 void
-gistbuildempty(Relation index)
+gistbuildempty(Relation index, ForkNumber forknum)
 {
 	Buffer		buffer;
 
 	/* Initialize the root page */
-	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
 							   EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
 
 	/* Initialize and xlog buffer */
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index b2e34d2d45e..8271af63220 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -175,10 +175,10 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * metapage, nor the first bitmap page.
 	 */
 	sort_threshold = (maintenance_work_mem * (Size) 1024) / BLCKSZ;
-	if (index->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
-		sort_threshold = Min(sort_threshold, NBuffers);
-	else
+	if (RelationUsesLocalBuffers(index))
 		sort_threshold = Min(sort_threshold, NLocBuffer);
+	else
+		sort_threshold = Min(sort_threshold, NBuffers);
 
 	if (num_buckets >= sort_threshold)
 		buildstate.spool = _h_spoolinit(heap, index, num_buckets);
@@ -215,12 +215,12 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 }
 
 /*
- *	hashbuildempty() -- build an empty hash index in the initialization fork
+ *	hashbuildempty() -- build an empty hash index in the specified fork
  */
 void
-hashbuildempty(Relation index)
+hashbuildempty(Relation index, ForkNumber forknum)
 {
-	_hash_init(index, 0, INIT_FORKNUM);
+	_hash_init(index, 0, forknum);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 0abdd7b49f5..39d1070bad9 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -177,16 +177,16 @@ bthandler(PG_FUNCTION_ARGS)
 }
 
 /*
- *	btbuildempty() -- build an empty btree index in the initialization fork
+ *	btbuildempty() -- build an empty btree index in the specified fork
  */
 void
-btbuildempty(Relation index)
+btbuildempty(Relation index, ForkNumber forknum)
 {
 	bool		allequalimage = _bt_allequalimage(index, false);
 	BulkWriteState *bulkstate;
 	BulkWriteBuffer metabuf;
 
-	bulkstate = smgr_bulk_start_rel(index, INIT_FORKNUM);
+	bulkstate = smgr_bulk_start_rel(index, forknum);
 
 	/* Construct metapage. */
 	metabuf = smgr_bulk_get_buf(bulkstate);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index 780ef646a54..1a14f84174a 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -148,15 +148,15 @@ spgbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 }
 
 /*
- * Build an empty SPGiST index in the initialization fork
+ * Build an empty SPGiST index in the specified fork
  */
 void
-spgbuildempty(Relation index)
+spgbuildempty(Relation index, ForkNumber forknum)
 {
 	BulkWriteState *bulkstate;
 	BulkWriteBuffer buf;
 
-	bulkstate = smgr_bulk_start_rel(index, INIT_FORKNUM);
+	bulkstate = smgr_bulk_start_rel(index, forknum);
 
 	/* Construct metapage. */
 	buf = smgr_bulk_get_buf(bulkstate);
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index c9aff944a2e..6c9e36bbadf 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -560,7 +560,8 @@ XLogSimpleInsertInt64(RmgrId rmid, uint8 info, int64 value)
 XLogRecPtr
 XLogGetFakeLSN(Relation rel)
 {
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		rel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		/*
 		 * Temporary relations are only accessible in our session, so a simple
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 60078952a16..9e184d63d14 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -51,8 +51,10 @@
  */
 #include "postgres.h"
 
+#include "access/amapi.h"
 #include "access/genam.h"
 #include "access/parallel.h"
+#include "access/table.h"
 #include "access/tableam.h"
 #include "access/xact.h"
 #include "access/xlogutils.h"
@@ -137,6 +139,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 (immutable) */
 	GtrInfoHistory history;		/* history of rel's session-local info */
 	SubTransactionId started_subid; /* usage started in current xact */
 	SubTransactionId stopped_subid; /* usage ended with another subid set */
@@ -557,7 +560,7 @@ gtr_init_usage_tables(void)
  *	an existing entry was found.
  */
 static GtrUsageEntry *
-gtr_record_usage(Oid relid, bool *found)
+gtr_record_usage(Oid relid, char relkind, bool *found)
 {
 	GtrUsageEntry *local_entry;
 	GtrSharedUsageKey key;
@@ -579,6 +582,9 @@ gtr_record_usage(Oid relid, bool *found)
 	/* Flag the usage entry for eoxact cleanup */
 	EOXactUsageListAdd(relid);
 
+	/* Remember the relation's relkind */
+	local_entry->relkind = relkind;
+
 	/* Add/update shared usage entry */
 	key.dbid = MyDatabaseId;
 	key.relid = relid;
@@ -944,6 +950,33 @@ InitGlobalTempRelation(Relation relation)
 
 		if (relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS)
 			register_on_commit_action(relation->rd_id, ONCOMMIT_DELETE_ROWS);
+
+		/*
+		 * If it's an index, build an empty index in the main fork.
+		 *
+		 * If the table is not empty (can happen if another session added the
+		 * index after we populated the table), then mark it as invalid.  The
+		 * user will need to do a REINDEX to build it.
+		 *
+		 * Note that this check for a non-empty table, using the block count,
+		 * might give a false positive, if the table contains deleted tuples,
+		 * which would force an unnecessary reindex, but it doesn't seem worth
+		 * the effort to do a more thorough check.
+		 */
+		if (relation->rd_rel->relkind == RELKIND_INDEX)
+		{
+			Relation	heapRelation;
+			BlockNumber nblocks;
+
+			relation->rd_indam->ambuildempty(relation, MAIN_FORKNUM);
+
+			heapRelation = table_open(relation->rd_index->indrelid, AccessShareLock);
+			nblocks = RelationGetNumberOfBlocks(heapRelation);
+			table_close(heapRelation, AccessShareLock);
+
+			if (nblocks > 0)
+				relation->rd_index->indisvalid = false;
+		}
 	}
 
 	/* Track our use of the relation, if we haven't already done so */
@@ -968,7 +1001,8 @@ TrackGlobalTempRelation(Relation relation)
 	bool		found;
 
 	/* Record our use of the relation, if we haven't done so already */
-	entry = gtr_record_usage(relation->rd_id, &found);
+	entry = gtr_record_usage(relation->rd_id, relation->rd_rel->relkind,
+							 &found);
 
 	/*
 	 * For a new entry, fill out the session-local relation information, with
@@ -978,6 +1012,19 @@ TrackGlobalTempRelation(Relation relation)
 	{
 		COPY_PG_CLASS_GTR_INFO(relation->rd_rel, &entry->history.info);
 
+		/*
+		 * When creating a new index locally, relation->rd_index will be NULL
+		 * here.  Mark the index 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) --- see
+		 * InitGlobalTempRelation().
+		 */
+		entry->history.info.indisvalid = (relation->rd_index == NULL ||
+										  relation->rd_index->indisvalid);
+
 		entry->history.subid = GetCurrentSubTransactionId();
 		entry->history.prev = NULL;
 	}
@@ -1508,11 +1555,49 @@ GetEffectivePgClassTuple(Oid relid)
 	return tuple;
 }
 
+/*
+ * GetEffectivePgIndexTuple
+ *
+ *	Get the effective pg_index tuple for an index relation.
+ *
+ *	This will fetch the pg_index tuple for the relation and then, if it's an
+ *	in-use global temporary relation, fetch the corresponding GtrInfo 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 GtrInfo, 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;
+
+	tuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexrelid));
+	if (HeapTupleIsValid(tuple))
+	{
+		Form_pg_index index_form = (Form_pg_index) GETSTRUCT(tuple);
+		GtrInfo    *gtr_info = GetGlobalTempRelationInfo(indexrelid);
+
+		if (gtr_info != NULL)
+			index_form->indisvalid = gtr_info->indisvalid;
+	}
+	return tuple;
+}
+
 /*
  * pg_gtr_info
  *
  *	SQL-callable function to retrieve the session-local information about an
  *	in-use global temporary relation.
+ *
+ *	Note: This only includes information overriding pg_class values, not the
+ *	pg_index.indisvalid override.
  */
 Datum
 pg_gtr_info(PG_FUNCTION_ARGS)
@@ -1543,6 +1628,9 @@ pg_gtr_info(PG_FUNCTION_ARGS)
  *
  *	SQL-callable function to retrieve the session-local information about all
  *	in-use global temporary relations.
+ *
+ *	Note: This only includes information overriding pg_class values, not the
+ *	pg_index.indisvalid override.
  */
 Datum
 pg_gtrs_in_use(PG_FUNCTION_ARGS)
@@ -1581,3 +1669,26 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 	}
 	return (Datum) 0;
 }
+
+/*
+ * pg_gtr_index_is_valid
+ *
+ *	SQL-callable function to test if a global temporary index relation is
+ *	valid in the current session.
+ */
+Datum
+pg_gtr_index_is_valid(PG_FUNCTION_ARGS)
+{
+	Oid			indexrelid = PG_GETARG_OID(0);
+	GtrUsageEntry *entry;
+
+	entry = FIND_LOCAL_USAGE_ENTRY(indexrelid);
+	if (entry == NULL || entry->stopped_subid != InvalidSubTransactionId)
+		PG_RETURN_NULL();
+
+	if (entry->relkind != RELKIND_INDEX &&
+		entry->relkind != RELKIND_PARTITIONED_INDEX)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(entry->history.info.indisvalid);
+}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 8ae1642b9dd..0b3f4dba922 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -37,6 +37,7 @@
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
+#include "catalog/global_temp.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/objectaccess.h"
@@ -120,7 +121,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);
@@ -572,7 +574,8 @@ UpdateIndexRelation(Oid indexoid,
 					bool isexclusion,
 					bool immediate,
 					bool isvalid,
-					bool isready)
+					bool isready,
+					char relpersistence)
 {
 	int2vector *indkey;
 	oidvector  *indcollation;
@@ -673,6 +676,19 @@ UpdateIndexRelation(Oid indexoid,
 	 */
 	table_close(pg_index, RowExclusiveLock);
 	heap_freetuple(tuple);
+
+	/*
+	 * For an index on a global temporary table, TrackGlobalTempRelation()
+	 * will have marked the index as valid.  If that's not actually the case,
+	 * fix that now.
+	 */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP && !isvalid)
+	{
+		GtrInfo    *gtr_info;
+
+		gtr_info = GetGlobalTempRelationInfoForUpdate(indexoid);
+		gtr_info->indisvalid = false;
+	}
 }
 
 
@@ -1061,7 +1077,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
@@ -2172,7 +2189,8 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 	 * lock (see comments in RemoveRelations), and a non-concurrent DROP is
 	 * more efficient.
 	 */
-	Assert(get_rel_persistence(indexId) != RELPERSISTENCE_TEMP ||
+	Assert((get_rel_persistence(indexId) != RELPERSISTENCE_TEMP &&
+			get_rel_persistence(indexId) != RELPERSISTENCE_GLOBAL_TEMP) ||
 		   (!concurrent && !concurrent_lock_mode));
 
 	/*
@@ -3141,7 +3159,7 @@ index_build(Relation heapRelation,
 	{
 		smgrcreate(RelationGetSmgr(indexRelation), INIT_FORKNUM, false);
 		log_smgrcreate(&indexRelation->rd_locator, INIT_FORKNUM);
-		indexRelation->rd_indam->ambuildempty(indexRelation);
+		indexRelation->rd_indam->ambuildempty(indexRelation, INIT_FORKNUM);
 	}
 
 	/*
@@ -3556,6 +3574,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);
 
@@ -3899,8 +3920,14 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
 		Relation	pg_index;
 		HeapTuple	indexTuple;
 		Form_pg_index indexForm;
+		GtrInfo    *gtr_info;
 		bool		index_bad;
 
+		/*
+		 * For a global temporary index, we update indisvalid in both pg_index
+		 * and the session-local GtrInfo struct, so that the change applies to
+		 * this session and all future sessions.
+		 */
 		pg_index = table_open(IndexRelationId, RowExclusiveLock);
 
 		indexTuple = SearchSysCacheCopy1(INDEXRELID,
@@ -3909,7 +3936,12 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
 			elog(ERROR, "cache lookup failed for index %u", indexId);
 		indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
 
-		index_bad = (!indexForm->indisvalid ||
+		if (RELATION_IS_GLOBAL_TEMP(iRel))
+			gtr_info = GetGlobalTempRelationInfoForUpdate(indexId);
+		else
+			gtr_info = NULL;
+
+		index_bad = (!GetEffective_indisvalid(indexForm, gtr_info) ||
 					 !indexForm->indisready ||
 					 !indexForm->indislive);
 		if (index_bad ||
@@ -3920,6 +3952,8 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
 			else if (index_bad)
 				indexForm->indcheckxmin = true;
 			indexForm->indisvalid = true;
+			if (gtr_info != NULL)
+				gtr_info->indisvalid = true;
 			indexForm->indisready = true;
 			indexForm->indislive = true;
 			CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index baea283aed1..026fdc8442b 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -270,7 +270,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 +283,7 @@ CheckIndexCompatible(Oid oldId,
 		  heap_attisnull(tuple, Anum_pg_index_indexprs, NULL) &&
 		  indexForm->indisvalid))
 	{
-		ReleaseSysCache(tuple);
+		heap_freetuple(tuple);
 		return false;
 	}
 
@@ -300,7 +300,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;
@@ -627,8 +627,16 @@ DefineIndex(ParseState *pstate,
 	 * is more efficient.  Do this before any use of the concurrent option is
 	 * done.
 	 */
-	if (stmt->concurrent && get_rel_persistence(tableId) != RELPERSISTENCE_TEMP)
-		concurrent = true;
+	if (stmt->concurrent)
+	{
+		char		relpersistence = get_rel_persistence(tableId);
+
+		if (relpersistence == RELPERSISTENCE_TEMP ||
+			relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			concurrent = false;
+		else
+			concurrent = true;
+	}
 	else
 		concurrent = false;
 
@@ -1592,6 +1600,11 @@ DefineIndex(ParseState *pstate,
 				HeapTuple	tup,
 							newtup;
 
+				/*
+				 * For a global temporary index, we update indisvalid in both
+				 * pg_index and the session-local GtrInfo struct, so that the
+				 * change applies to this session and all future sessions.
+				 */
 				tup = SearchSysCache1(INDEXRELID,
 									  ObjectIdGetDatum(indexRelationId));
 				if (!HeapTupleIsValid(tup))
@@ -1604,6 +1617,14 @@ DefineIndex(ParseState *pstate,
 				table_close(pg_index, RowExclusiveLock);
 				heap_freetuple(newtup);
 
+				if (rel_is_global_temp(indexRelationId))
+				{
+					GtrInfo    *gtr_info;
+
+					gtr_info = GetGlobalTempRelationInfoForUpdate(indexRelationId);
+					gtr_info->indisvalid = false;
+				}
+
 				/*
 				 * CCI here to make this update visible, in case this recurses
 				 * across multiple partition levels.
@@ -3136,7 +3157,8 @@ ReindexIndex(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLev
 	if (relkind == RELKIND_PARTITIONED_INDEX)
 		ReindexPartitions(stmt, indOid, params, isTopLevel);
 	else if ((params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
-			 persistence != RELPERSISTENCE_TEMP)
+			 persistence != RELPERSISTENCE_TEMP &&
+			 persistence != RELPERSISTENCE_GLOBAL_TEMP)
 		ReindexRelationConcurrently(stmt, indOid, params);
 	else
 	{
@@ -3232,6 +3254,7 @@ static Oid
 ReindexTable(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLevel)
 {
 	Oid			heapOid;
+	char		persistence;
 	bool		result;
 	const RangeVar *relation = stmt->relation;
 
@@ -3249,10 +3272,13 @@ ReindexTable(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLev
 									   0,
 									   RangeVarCallbackMaintainsTable, NULL);
 
+	persistence = get_rel_persistence(heapOid);
+
 	if (get_rel_relkind(heapOid) == RELKIND_PARTITIONED_TABLE)
 		ReindexPartitions(stmt, heapOid, params, isTopLevel);
 	else if ((params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
-			 get_rel_persistence(heapOid) != RELPERSISTENCE_TEMP)
+			 persistence != RELPERSISTENCE_TEMP &&
+			 persistence != RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		result = ReindexRelationConcurrently(stmt, heapOid, params);
 
@@ -3681,7 +3707,8 @@ ReindexMultipleInternal(const ReindexStmt *stmt, const List *relids, const Reind
 		Assert(!RELKIND_HAS_PARTITIONS(relkind));
 
 		if ((params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
-			relpersistence != RELPERSISTENCE_TEMP)
+			relpersistence != RELPERSISTENCE_TEMP &&
+			relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
 		{
 			ReindexParams newparams = *params;
 
@@ -4123,7 +4150,8 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 		idx->amId = indexRel->rd_rel->relam;
 
 		/* This function shouldn't be called for temporary relations. */
-		if (indexRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		if (indexRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+			indexRel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 			elog(ERROR, "cannot reindex a temporary table concurrently");
 
 		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX, idx->tableId);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index ab03eafbfa9..6da84baed44 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -868,8 +868,15 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 		}
 		else if (thisIndexOid == indexOid)
 		{
+			GtrInfo    *gtr_info;
+
+			if (RELATION_IS_GLOBAL_TEMP(rel))
+				gtr_info = GetGlobalTempRelationInfo(thisIndexOid);
+			else
+				gtr_info = NULL;
+
 			/* this was checked earlier, but let's be real sure */
-			if (!indexForm->indisvalid)
+			if (!GetEffective_indisvalid(indexForm, gtr_info))
 				elog(ERROR, "cannot cluster on invalid index %u", indexOid);
 			indexForm->indisclustered = true;
 			CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b4671c37280..c4cac6c45d6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1827,7 +1827,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);
@@ -1836,7 +1836,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)
@@ -9847,8 +9847,9 @@ verifyNotNullPKCompatible(HeapTuple tuple, const char *colname)
  * ALTER TABLE ADD INDEX
  *
  * There is no such command in the grammar, but parse_utilcmd.c converts
- * UNIQUE and PRIMARY KEY constraints into AT_AddIndex subcommands.  This lets
- * us schedule creation of the index at the appropriate time during ALTER.
+ * UNIQUE, PRIMARY KEY, and EXCLUSION constraints into AT_AddIndex
+ * subcommands.  This lets us schedule creation of the index at the
+ * appropriate time during ALTER.
  *
  * Return value is the address of the new index.
  */
@@ -9867,6 +9868,29 @@ ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
 	/* The IndexStmt has already been through transformIndexStmt */
 	Assert(stmt->transformed);
 
+	/*
+	 * 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.  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
+	 * it would be OK to allow UNIQUE and EXCLUSION constraints to be added,
+	 * since the indexes enforcing them would be marked invalid in any other
+	 * sessions with data in the table, so this would effectively just be
+	 * adding a NOT VALID constraint in those other sessions, which could be
+	 * validated by doing a REINDEX.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(rel) &&
+		IsOtherUsingGlobalTempRelation(RelationGetRelid(rel)))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot add or alter constraints of global temporary table \"%s\" because it is being used in another session",
+					   RelationGetRelationName(rel)));
+
 	/* suppress schema rights check when rebuilding existing index */
 	check_rights = !is_rebuild;
 	/* skip index build if phase 3 will do it or we're reusing an old one */
@@ -14052,7 +14076,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);
@@ -14072,7 +14096,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 			*indexOid = indexoid;
 			break;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 	}
 
 	list_free(indexoidlist);
@@ -14110,7 +14134,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	*pk_has_without_overlaps = indexStruct->indisexclusion;
 
-	ReleaseSysCache(indexTuple);
+	heap_freetuple(indexTuple);
 
 	return i;
 }
@@ -14173,7 +14197,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);
@@ -14249,7 +14273,7 @@ transformFkeyCheckAttrs(Relation pkrel,
 			if (found)
 				*pk_has_without_overlaps = indexStruct->indisexclusion;
 		}
-		ReleaseSysCache(indexTuple);
+		heap_freetuple(indexTuple);
 		if (found)
 			break;
 	}
@@ -22713,14 +22737,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 */
@@ -22736,7 +22759,13 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
 		Relation	idxRel;
 		HeapTuple	indTup;
 		Form_pg_index indexForm;
+		GtrInfo    *gtr_info;
 
+		/*
+		 * For a global temporary index, we update indisvalid in both pg_index
+		 * and the session-local GtrInfo struct, so that the change applies to
+		 * this session and all future sessions.
+		 */
 		idxRel = table_open(IndexRelationId, RowExclusiveLock);
 		indTup = SearchSysCacheCopy1(INDEXRELID,
 									 ObjectIdGetDatum(RelationGetRelid(partedIdx)));
@@ -22745,7 +22774,14 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
 				 RelationGetRelid(partedIdx));
 		indexForm = (Form_pg_index) GETSTRUCT(indTup);
 
+		if (RELATION_IS_GLOBAL_TEMP(partedIdx))
+			gtr_info = GetGlobalTempRelationInfoForUpdate(RelationGetRelid(partedIdx));
+		else
+			gtr_info = NULL;
+
 		indexForm->indisvalid = true;
+		if (gtr_info != NULL)
+			gtr_info->indisvalid = true;
 		updated = true;
 
 		CatalogTupleUpdate(idxRel, &indTup->t_self, indTup);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8d30131855a..c17f9a8446c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -7300,6 +7300,7 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)
 	 * safe.
 	 */
 	if (heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		heap->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ||
 		!is_parallel_safe(root, (Node *) RelationGetIndexExpressions(index)) ||
 		!is_parallel_safe(root, (Node *) RelationGetIndexPredicate(index)))
 	{
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f838311090b..3ee37774da0 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -30,6 +30,7 @@
 #include "access/table.h"
 #include "access/toast_compression.h"
 #include "catalog/dependency.h"
+#include "catalog/global_temp.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/namespace.h"
@@ -1731,10 +1732,10 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 		*constraintOid = InvalidOid;
 
 	/*
-	 * Fetch pg_class tuple of source index.  We can't use the copy in the
-	 * relcache entry because it doesn't include optional fields.
+	 * Fetch effective pg_class tuple of source index.  We can't use the copy
+	 * in the relcache entry because it doesn't include optional fields.
 	 */
-	ht_idxrel = SearchSysCache1(RELOID, ObjectIdGetDatum(source_relid));
+	ht_idxrel = GetEffectivePgClassTuple(source_relid);
 	if (!HeapTupleIsValid(ht_idxrel))
 		elog(ERROR, "cache lookup failed for relation %u", source_relid);
 	idxrelrec = (Form_pg_class) GETSTRUCT(ht_idxrel);
@@ -2049,7 +2050,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	}
 
 	/* Clean up */
-	ReleaseSysCache(ht_idxrel);
+	heap_freetuple(ht_idxrel);
 	ReleaseSysCache(ht_am);
 
 	return index;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 09e1d220775..2d84875ed23 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2413,6 +2413,17 @@ get_rel_persistence(Oid relid)
 	return result;
 }
 
+/*
+ * rel_is_global_temp
+ *
+ *		Returns true if the given relation is a global temporary relation.
+ */
+bool
+rel_is_global_temp(Oid relid)
+{
+	return get_rel_persistence(relid) == RELPERSISTENCE_GLOBAL_TEMP;
+}
+
 /*
  * get_rel_relam
  *
@@ -3925,13 +3936,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 3a04ccbc271..abcb038747c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1489,8 +1489,7 @@ RelationInitIndexAccessInfo(Relation relation)
 	 * contains variable-length and possibly-null fields, we have to do this
 	 * honestly rather than just treating it as a Form_pg_index struct.
 	 */
-	tuple = SearchSysCache1(INDEXRELID,
-							ObjectIdGetDatum(RelationGetRelid(relation)));
+	tuple = GetEffectivePgIndexTuple(RelationGetRelid(relation));
 	if (!HeapTupleIsValid(tuple))
 		elog(ERROR, "cache lookup failed for index %u",
 			 RelationGetRelid(relation));
@@ -1498,7 +1497,7 @@ RelationInitIndexAccessInfo(Relation relation)
 	relation->rd_indextuple = heap_copytuple(tuple);
 	relation->rd_index = (Form_pg_index) GETSTRUCT(relation->rd_indextuple);
 	MemoryContextSwitchTo(oldcontext);
-	ReleaseSysCache(tuple);
+	heap_freetuple(tuple);
 
 	/*
 	 * Look up the index's access method, save the OID of its handler function
@@ -2398,8 +2397,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));
@@ -2427,7 +2425,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 */
@@ -4979,6 +4977,7 @@ RelationGetIndexList(Relation relation)
 	while (HeapTupleIsValid(htup = systable_getnext(indscan)))
 	{
 		Form_pg_index index = (Form_pg_index) GETSTRUCT(htup);
+		bool		indisvalid;
 
 		/*
 		 * Ignore any indexes that are currently being dropped.  This will
@@ -5002,6 +5001,19 @@ RelationGetIndexList(Relation relation)
 			!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
 			continue;
 
+		/*
+		 * Global temporary indexes may override indisvalid locally.
+		 */
+		indisvalid = index->indisvalid;
+		if (RELATION_IS_GLOBAL_TEMP(relation))
+		{
+			GtrInfo    *gtr_info;
+
+			gtr_info = GetGlobalTempRelationInfo(index->indexrelid);
+			if (gtr_info != NULL)
+				indisvalid = gtr_info->indisvalid;
+		}
+
 		/*
 		 * 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
@@ -5013,7 +5025,7 @@ RelationGetIndexList(Relation relation)
 		 * partitioned tables.
 		 */
 		if (index->indisprimary &&
-			(index->indisvalid ||
+			(indisvalid ||
 			 relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
 		{
 			pkeyIndex = index->indexrelid;
@@ -5023,7 +5035,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 878c4192076..1ab7c782d3b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2269,8 +2269,18 @@ 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,
+								 "CASE WHEN c.relpersistence = "
+								 CppAsString2(RELPERSISTENCE_GLOBAL_TEMP)
+								 " THEN COALESCE(pg_catalog.pg_gtr_index_is_valid(c.oid),"
+								 " i.indisvalid) ELSE i.indisvalid END,\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 "
@@ -2389,7 +2399,16 @@ 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,
+									 "CASE WHEN c2.relpersistence = "
+									 CppAsString2(RELPERSISTENCE_GLOBAL_TEMP)
+									 " THEN COALESCE(pg_catalog.pg_gtr_index_is_valid(c2.oid),"
+									 " i.indisvalid) ELSE i.indisvalid END, ");
+			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");
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index d7fb16d3c85..70da07d2369 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -655,6 +655,8 @@ get_parallel_tables_list(PGconn *conn, ReindexType type,
 								 CppAsString2(RELKIND_MATVIEW) ")\n"
 								 "   AND c.relpersistence != "
 								 CppAsString2(RELPERSISTENCE_TEMP) "\n"
+								 "   AND c.relpersistence != "
+								 CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) "\n"
 								 " ORDER BY c.relpages DESC;");
 			break;
 
@@ -678,6 +680,8 @@ get_parallel_tables_list(PGconn *conn, ReindexType type,
 									 CppAsString2(RELKIND_MATVIEW) ")\n"
 									 "   AND c.relpersistence != "
 									 CppAsString2(RELPERSISTENCE_TEMP) "\n"
+									 "   AND c.relpersistence != "
+									 CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) "\n"
 									 " AND ns.nspname IN (");
 
 				for (cell = user_list->head; cell; cell = cell->next)
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 79240333530..b3b9cb6039b 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -15,6 +15,7 @@
 #include "access/cmptype.h"
 #include "access/genam.h"
 #include "access/stratnum.h"
+#include "common/relpath.h"
 #include "nodes/nodes.h"
 #include "nodes/pg_list.h"
 
@@ -115,7 +116,8 @@ typedef IndexBuildResult *(*ambuild_function) (Relation heapRelation,
 											   IndexInfo *indexInfo);
 
 /* build empty index */
-typedef void (*ambuildempty_function) (Relation indexRelation);
+typedef void (*ambuildempty_function) (Relation indexRelation,
+									   ForkNumber forknum);
 
 /* insert this tuple */
 typedef bool (*aminsert_function) (Relation indexRelation,
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e17c7fee511..10137275c4c 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -90,7 +90,7 @@ extern BrinDesc *brin_build_desc(Relation rel);
 extern void brin_free_desc(BrinDesc *bdesc);
 extern IndexBuildResult *brinbuild(Relation heap, Relation index,
 								   IndexInfo *indexInfo);
-extern void brinbuildempty(Relation index);
+extern void brinbuildempty(Relation index, ForkNumber forknum);
 extern bool brininsert(Relation idxRel, Datum *values, bool *nulls,
 					   ItemPointer heaptid, Relation heapRel,
 					   IndexUniqueCheck checkUnique,
diff --git a/src/include/access/gin_private.h b/src/include/access/gin_private.h
index 3c5fd6ba817..dd51cb4453e 100644
--- a/src/include/access/gin_private.h
+++ b/src/include/access/gin_private.h
@@ -110,7 +110,7 @@ extern char *ginbuildphasename(int64 phasenum);
 /* gininsert.c */
 extern IndexBuildResult *ginbuild(Relation heap, Relation index,
 								  struct IndexInfo *indexInfo);
-extern void ginbuildempty(Relation index);
+extern void ginbuildempty(Relation index, ForkNumber forknum);
 extern bool gininsert(Relation index, Datum *values, bool *isnull,
 					  ItemPointer ht_ctid, Relation heapRel,
 					  IndexUniqueCheck checkUnique,
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index 39fdd27f5b9..69acc189592 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -399,7 +399,7 @@ typedef struct GiSTOptions
 } GiSTOptions;
 
 /* gist.c */
-extern void gistbuildempty(Relation index);
+extern void gistbuildempty(Relation index, ForkNumber forknum);
 extern bool gistinsert(Relation r, Datum *values, bool *isnull,
 					   ItemPointer ht_ctid, Relation heapRel,
 					   IndexUniqueCheck checkUnique,
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0e5ea..a719d374af1 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -362,7 +362,7 @@ typedef struct HashOptions
 
 extern IndexBuildResult *hashbuild(Relation heap, Relation index,
 								   struct IndexInfo *indexInfo);
-extern void hashbuildempty(Relation index);
+extern void hashbuildempty(Relation index, ForkNumber forknum);
 extern bool hashinsert(Relation rel, Datum *values, bool *isnull,
 					   ItemPointer ht_ctid, Relation heapRel,
 					   IndexUniqueCheck checkUnique,
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 3097e9bb1af..8053dfe3d6a 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1151,7 +1151,7 @@ typedef struct BTOptions
 /*
  * external entry points for btree, in nbtree.c
  */
-extern void btbuildempty(Relation index);
+extern void btbuildempty(Relation index, ForkNumber forknum);
 extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 ItemPointer ht_ctid, Relation heapRel,
 					 IndexUniqueCheck checkUnique,
diff --git a/src/include/access/spgist.h b/src/include/access/spgist.h
index 083d93f8ffd..392c6538cbf 100644
--- a/src/include/access/spgist.h
+++ b/src/include/access/spgist.h
@@ -195,7 +195,7 @@ extern bytea *spgoptions(Datum reloptions, bool validate);
 /* spginsert.c */
 extern IndexBuildResult *spgbuild(Relation heap, Relation index,
 								  struct IndexInfo *indexInfo);
-extern void spgbuildempty(Relation index);
+extern void spgbuildempty(Relation index, ForkNumber forknum);
 extern bool spginsert(Relation index, Datum *values, bool *isnull,
 					  ItemPointer ht_ctid, Relation heapRel,
 					  IndexUniqueCheck checkUnique,
diff --git a/src/include/catalog/global_temp.h b/src/include/catalog/global_temp.h
index 5b41f9a7ecd..63c23a6930c 100644
--- a/src/include/catalog/global_temp.h
+++ b/src/include/catalog/global_temp.h
@@ -21,12 +21,17 @@
  *
  *	Structure holding information about a global temporary relation that is
  *	local to the current session.  These properties act as local overrides to
- *	the information stored in pg_class, allowing it to vary between backends.
+ *	the information stored in pg_class and pg_index, allowing it to vary
+ * between backends.
  */
 typedef struct GtrInfo
 {
+	/* pg_class info */
 	Oid			relfilenode;	/* the relation's physical storage file */
 	Oid			reltablespace;	/* the relation's tablespace identifier */
+
+	/* pg_index info */
+	bool		indisvalid;		/* is the index valid in this session? */
 } GtrInfo;
 
 /*
@@ -61,6 +66,7 @@ extern List *GetAllGlobalTempRelationsInUse(Oid dbId);
 extern GtrInfo *GetGlobalTempRelationInfo(Oid relid);
 extern GtrInfo *GetGlobalTempRelationInfoForUpdate(Oid relid);
 extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+extern HeapTuple GetEffectivePgIndexTuple(Oid indexrelid);
 
 /*
  * Get the effective value of relfilenode for a relation.  For a global
@@ -82,6 +88,16 @@ GetEffective_reltablespace(Form_pg_class class_form, GtrInfo *gtr_info)
 	return gtr_info != NULL ? gtr_info->reltablespace : class_form->reltablespace;
 }
 
+/*
+ * Get the effective value of indisvalid for an index relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline bool
+GetEffective_indisvalid(Form_pg_index index_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->indisvalid : index_form->indisvalid;
+}
+
 /*
  * Set the effective value of relfilenode for a relation.  For a global
  * temporary relation, GetGlobalTempRelationInfoForUpdate() should have been
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd4cad0dddf..852654de5b8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12793,5 +12793,8 @@
   proargmodes => '{o,o,o}',
   proargnames => '{oid,relfilenode,reltablespace}',
   prosrc => 'pg_gtrs_in_use' },
+{ oid => '8084', descr => 'is an in-use global temporary index relation valid?',
+  proname => 'pg_gtr_index_is_valid', provolatile => 's', proparallel => 'u',
+  prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_gtr_index_is_valid' },
 
 ]
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index aea7311faad..b216956f193 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3543,7 +3543,8 @@ typedef struct IndexStmt
 	bool		unique;			/* is index unique? */
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
 	bool		primary;		/* is index a primary key? */
-	bool		isconstraint;	/* is it for a pkey/unique constraint? */
+	bool		isconstraint;	/* is it for a pkey/unique/exclusion
+								 * constraint? */
 	bool		iswithoutoverlaps;	/* is the constraint WITHOUT OVERLAPS? */
 	bool		deferrable;		/* is the constraint DEFERRABLE? */
 	bool		initdeferred;	/* is the constraint INITIALLY DEFERRED? */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 171fbda9a71..7c13b41243f 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -150,6 +150,7 @@ extern char get_rel_relkind(Oid relid);
 extern bool get_rel_relispartition(Oid relid);
 extern Oid	get_rel_tablespace(Oid relid);
 extern char get_rel_persistence(Oid relid);
+extern bool rel_is_global_temp(Oid relid);
 extern Oid	get_rel_relam(Oid relid);
 extern Oid	get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
 extern Oid	get_transform_tosql(Oid typid, Oid langid, List *trftypes);
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 7fceab3bb4c..ccab38b8c59 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -210,13 +210,17 @@ key|val
 (0 rows)
 
 
-starting permutation: create1 ins1_2 alter1a alter1b alter1c alter1d ins2_2 seltype1 seltype2 drop1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+starting permutation: create1 ins1_2 alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h ins2_2 seltype1 seltype2 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 alter1a: ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric;
 step alter1b: ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL;
 step alter1c: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key;
 step alter1d: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_chk CHECK (key > 0);
+step alter1e: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_pk PRIMARY KEY (key);
+step alter1f: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_un UNIQUE (val);
+step alter1g: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_fk FOREIGN KEY (icol) REFERENCES tmp;
+step alter1h: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_ex EXCLUDE USING gist (bcol WITH &&);
 step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
 step seltype1: SELECT key, pg_typeof(key), val FROM tmp2;
 key|pg_typeof|val
@@ -232,8 +236,8 @@ key|pg_typeof|val
 
 step drop1: DROP TABLE tmp2;
 
-starting permutation: create1 ins1_2 ins2_2 alter1a alter1b alter1c alter1d seltype1 seltype2 drop1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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');
 step alter1a: ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric;
@@ -244,6 +248,15 @@ step alter1c: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key;
 ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
 step alter1d: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_chk CHECK (key > 0);
 ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step alter1e: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_pk PRIMARY KEY (key);
+ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step alter1f: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_un UNIQUE (val);
+ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step alter1g: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_fk FOREIGN KEY (icol) REFERENCES tmp;
+ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step alter1h: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_ex EXCLUDE USING gist (bcol WITH &&);
+ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step uniq_idx1: CREATE UNIQUE INDEX tmp2_un ON tmp2(val);
 step seltype1: SELECT key, pg_typeof(key), val FROM tmp2;
 key|pg_typeof|val
 ---+---------+---
@@ -256,6 +269,34 @@ key|pg_typeof|val
   1|integer  |s2 
 (1 row)
 
+step idx_valid1: 
+  SELECT indisvalid, pg_gtr_index_is_valid(indexrelid)
+    FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
+
+indisvalid|pg_gtr_index_is_valid
+----------+---------------------
+t         |t                    
+(1 row)
+
+step idx_valid2: 
+  SELECT indisvalid, pg_gtr_index_is_valid(indexrelid)
+    FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
+
+indisvalid|pg_gtr_index_is_valid
+----------+---------------------
+t         |f                    
+(1 row)
+
+step uniq_reidx2: REINDEX INDEX tmp2_un;
+step idx_valid2: 
+  SELECT indisvalid, pg_gtr_index_is_valid(indexrelid)
+    FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
+
+indisvalid|pg_gtr_index_is_valid
+----------+---------------------
+t         |t                    
+(1 row)
+
 step drop1: DROP TABLE tmp2;
 
 starting permutation: create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
@@ -269,14 +310,14 @@ step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
 step drop1: DROP TABLE tmp2;
 
 starting permutation: create1 drop2 b1 prep1 cprep1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
 step drop2: DROP TABLE tmp2;
 step b1: BEGIN;
 step prep1: PREPARE TRANSACTION 'tx';
 step cprep1: COMMIT PREPARED 'tx';
 
 starting permutation: create1 ins1_2 b1 drop2 prep1 cprep1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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 b1: BEGIN;
 step drop2: DROP TABLE tmp2;
@@ -284,7 +325,7 @@ step prep1: PREPARE TRANSACTION 'tx';
 step cprep1: COMMIT PREPARED 'tx';
 
 starting permutation: create1 b1 ins1_2 drop2 prep1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
 step b1: BEGIN;
 step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
 step drop2: DROP TABLE tmp2; <waiting ...>
@@ -381,7 +422,7 @@ key|val
 step reset_tblspace: ALTER TABLE tmp SET TABLESPACE pg_default;
 
 starting permutation: create1 ins1_2 used1 drop1 used1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
@@ -407,7 +448,7 @@ regexp_replace
 
 
 starting permutation: create1 ins1_2 used1 drop2 used1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
@@ -433,7 +474,7 @@ regexp_replace
 
 
 starting permutation: create1 ins1_2 used1 b1 drop2 used1 r1 used1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
@@ -470,7 +511,7 @@ regexp_replace
 
 
 starting permutation: create1 ins1_2 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
-step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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 b1: BEGIN;
 step used1: 
@@ -517,6 +558,231 @@ regexp_replace
 (0 rows)
 
 
+starting permutation: ins1 idx1 sel1_idx ins2 sel2_idx
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+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
+---+---
+  1|s1 
+(1 row)
+
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+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
+---+---
+  1|s2 
+(1 row)
+
+
+starting permutation: ins1 ins2 idx1 sel1_idx 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
+---+---
+  1|s1 
+(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
+---+---
+  1|s2 
+(1 row)
+
+
+starting permutation: ins1 ins2 idx1 sel1_idx 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
+---+---
+  1|s1 
+(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
+---+---
+  1|s2 
+(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
+---+---
+  1|s2 
+(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
+---+---
+  1|s1 
+(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
+---+---
+  1|s2 
+(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
+---+---
+  1|s2 
+(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
+---+---
+  1|s2 
+(1 row)
+
+
 starting permutation: drop_tblspace list_tblspaces
 step drop_tblspace: DROP TABLESPACE regress_isolation_tablespace;
 step list_tblspaces: SELECT spcname FROM pg_tablespace ORDER BY 1;
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index 01b63fdaf9d..850432fda4d 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -1,9 +1,9 @@
 # Test global temporary relations
 
 setup {
-  CREATE GLOBAL TEMP TABLE tmp (key int, val text);
+  CREATE GLOBAL TEMP TABLE tmp (key int PRIMARY KEY, val text);
 
-  CREATE GLOBAL TEMP TABLE tmp_parted (key int, val text) PARTITION BY LIST (key);
+  CREATE GLOBAL TEMP TABLE tmp_parted (key int PRIMARY KEY, val text) PARTITION BY LIST (key);
   CREATE GLOBAL TEMP TABLE tmp_p1 PARTITION OF tmp_parted FOR VALUES IN (1);
   CREATE GLOBAL TEMP TABLE tmp_p2 PARTITION OF tmp_parted FOR VALUES IN ((2), (3));
 }
@@ -35,14 +35,23 @@ step ins1p2 { INSERT INTO tmp_parted VALUES (2, 's1 p2'); }
 step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
 
 # Test prevention of ALTER TABLE with rewrite, if in use
-step create1 { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text); }
+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 alter1a { ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric; }
 step alter1b { ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL; }
 step alter1c { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key; }
 step alter1d { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_chk CHECK (key > 0); }
+step alter1e { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_pk PRIMARY KEY (key); }
+step alter1f { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_un UNIQUE (val); }
+step alter1g { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_fk FOREIGN KEY (icol) REFERENCES tmp; }
+step alter1h { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_ex EXCLUDE USING gist (bcol WITH &&); }
 step seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
 step drop1 { DROP TABLE tmp2; }
+step uniq_idx1 { CREATE UNIQUE INDEX tmp2_un ON tmp2(val); }
+step idx_valid1 {
+  SELECT indisvalid, pg_gtr_index_is_valid(indexrelid)
+    FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
+}
 
 # Test DROP with ON COMMIT DELETE ROWS
 step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
@@ -70,6 +79,16 @@ step used1 {
    ORDER BY 1;
 }
 
+# Test val index
+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';
+}
+
 session s2
 # Transaction control
 step b2 { BEGIN; }
@@ -88,6 +107,11 @@ step sel2p { SELECT tableoid::regclass, * FROM tmp_parted; }
 # Test prevention of ALTER TABLE with rewrite, if in use
 step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
 step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step uniq_reidx2 { REINDEX INDEX tmp2_un; }
+step idx_valid2 {
+  SELECT indisvalid, pg_gtr_index_is_valid(indexrelid)
+    FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
+}
 
 # Test GTT inval in prepared transaction
 step drop2 { DROP TABLE tmp2; }
@@ -106,6 +130,17 @@ step get_tblspace2 {
    WHERE c.relname = 'tmp';
 }
 
+# Test val index
+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';
+}
+step reidx2 { REINDEX INDEX tmp_val_idx; }
+step analyze2 { ANALYZE tmp; }
+
 # Create test tablespace for remaining tests
 permutation create_tblspace list_tblspaces
 
@@ -122,11 +157,12 @@ permutation ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
 
 # Test prevention of ALTER TABLE with rewrite, if in use
 permutation create1 ins1_2
-            alter1a alter1b alter1c alter1d
+            alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h
             ins2_2 seltype1 seltype2 drop1
 permutation create1 ins1_2 ins2_2
-            alter1a alter1b alter1c alter1d
-            seltype1 seltype2 drop1
+            alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h
+            uniq_idx1 seltype1 seltype2
+            idx_valid1 idx_valid2 uniq_reidx2 idx_valid2 drop1
 
 # Test DROP with ON COMMIT DELETE ROWS
 permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
@@ -149,5 +185,11 @@ permutation create1 ins1_2 used1 drop2 used1
 permutation create1 ins1_2 used1 b1 drop2 used1 r1 used1
 permutation create1 ins1_2 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
 
+# Test val index
+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
+
 # Tidy up
 permutation drop_tblspace list_tblspaces
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 31f8d2b8161..65b116280bf 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -166,10 +166,10 @@ dibuild(Relation heap, Relation index, IndexInfo *indexInfo)
 }
 
 /*
- * Build an empty index for the initialization fork.
+ * Build an empty index for the specified fork.
  */
 static void
-dibuildempty(Relation index)
+dibuildempty(Relation index, ForkNumber forknum)
 {
 	/* No need to build an init fork for a dummy index */
 }
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 493b0f96f81..44ad16684a8 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -13,7 +13,7 @@ CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
 ERROR:  cannot create global temporary relation in temporary schema
 LINE 1: CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int);
                                  ^
-CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text);
 CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
 CREATE SCHEMA global_temp_yyy;
 CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
@@ -21,15 +21,18 @@ CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
   Global temporary table "global_temp_tests.tmp1"
  Column |  Type   | Collation | Nullable | Default 
 --------+---------+-----------+----------+---------
- a      | integer |           |          | 
+ a      | integer |           | not null | 
+ b      | text    |           |          | 
+Indexes:
+    "tmp1_pkey" PRIMARY KEY, btree (a)
 
 \dt+ global_temp_*.tmp*
-                                             List of tables
-      Schema       | Name | Type  |          Owner           |   Persistence    |  Size   | Description 
--------------------+------+-------+--------------------------+------------------+---------+-------------
- global_temp_tests | tmp1 | table | regress_global_temp_user | global temporary | 0 bytes | 
- global_temp_xxx   | tmp2 | table | regress_global_temp_user | global temporary | 0 bytes | 
- global_temp_yyy   | tmp3 | table | regress_global_temp_user | global temporary | 0 bytes | 
+                                              List of tables
+      Schema       | Name | Type  |          Owner           |   Persistence    |    Size    | Description 
+-------------------+------+-------+--------------------------+------------------+------------+-------------
+ global_temp_tests | tmp1 | table | regress_global_temp_user | global temporary | 8192 bytes | 
+ global_temp_xxx   | tmp2 | table | regress_global_temp_user | global temporary | 0 bytes    | 
+ global_temp_yyy   | tmp3 | table | regress_global_temp_user | global temporary | 0 bytes    | 
 (3 rows)
 
 -- Information schema
@@ -49,18 +52,18 @@ NOTICE:  drop cascades to table global_temp_xxx.tmp2
 DROP SCHEMA global_temp_yyy CASCADE;
 NOTICE:  drop cascades to table global_temp_yyy.tmp3
 -- Basic tests
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
 SELECT * FROM tmp1;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 \c
 SET search_path = global_temp_tests;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 -- Test pg_gtr_info() and pg_gtrs_in_use()
@@ -78,8 +81,8 @@ SELECT * FROM pg_gtrs_in_use();
 (0 rows)
 
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 SELECT c.relfilenode = c.oid,
@@ -98,11 +101,65 @@ SELECT c.relname,
        t.reltablespace = c.reltablespace
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
- relname | ?column? | ?column? 
----------+----------+----------
- tmp1    | t        | t
+  relname  | ?column? | ?column? 
+-----------+----------+----------
+ tmp1      | t        | t
+ tmp1_pkey | t        | t
+(2 rows)
+
+-- Test index
+INSERT INTO tmp1 VALUES (1, 'xxx');
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE a = 1;
+             QUERY PLAN             
+------------------------------------
+ Index Scan using tmp1_pkey on tmp1
+   Index Cond: (a = 1)
+(2 rows)
+
+SELECT * FROM tmp1 WHERE a = 1;
+ a |  b  
+---+-----
+ 1 | xxx
+(1 row)
+
+RESET enable_seqscan;
+-- Test concurrent index build -- CONCURRENTLY is ignored with temp tables
+CREATE INDEX CONCURRENTLY tmp1_b_idx ON tmp1(b);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE b = 'xxx';
+             QUERY PLAN              
+-------------------------------------
+ Index Scan using tmp1_b_idx on tmp1
+   Index Cond: (b = 'xxx'::text)
+(2 rows)
+
+SELECT * FROM tmp1 WHERE b = 'xxx';
+ a |  b  
+---+-----
+ 1 | xxx
+(1 row)
+
+RESET enable_seqscan;
+REINDEX INDEX CONCURRENTLY tmp1_b_idx;
+REINDEX TABLE CONCURRENTLY tmp1;
+-- Test REINDEX -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1_b_idx'::regclass \gset
+REINDEX INDEX tmp1_b_idx;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1_b_idx'::regclass;
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
 (1 row)
 
+DROP INDEX CONCURRENTLY tmp1_b_idx;
 -- Test ON COMMIT DELETE ROWS
 CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
 BEGIN;
@@ -143,8 +200,8 @@ ERROR:  ON COMMIT DROP cannot be used on global temporary tables
 -- Two-phase commit not allowed with global temp tables
 BEGIN;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 PREPARE TRANSACTION 'twophase'; -- fail
@@ -180,6 +237,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,
+       pg_gtr_index_is_valid(c.oid) AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ 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,
+       pg_gtr_index_is_valid(c.oid) AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ 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,
+       pg_gtr_index_is_valid(c.oid) AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ 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);
@@ -194,11 +354,33 @@ DROP TABLE tmp2;
 -- Test foreign keys
 CREATE TABLE perm_pk_rel (a int PRIMARY KEY);
 CREATE TEMP TABLE temp_pk_rel (a int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE gtemp_pk_rel (a int PRIMARY KEY);
+CREATE TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
+ERROR:  constraints on permanent tables may reference only permanent tables
+CREATE TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
+ERROR:  constraints on local temporary tables may reference only local temporary tables
 CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES perm_pk_rel); -- fail
 ERROR:  constraints on global temporary tables may reference only global temporary tables
 CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES temp_pk_rel); -- fail
 ERROR:  constraints on global temporary tables may reference only global temporary tables
-DROP TABLE perm_pk_rel, temp_pk_rel;
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel);
+INSERT INTO gtemp_pk_rel VALUES (1);
+INSERT INTO tmp2 VALUES (1);
+INSERT INTO tmp2 VALUES (2); -- fail
+ERROR:  insert or update on table "tmp2" violates foreign key constraint "tmp2_a_fkey"
+DETAIL:  Key (a)=(2) is not present in table "gtemp_pk_rel".
+DELETE FROM gtemp_pk_rel WHERE a = 1; -- fail
+ERROR:  update or delete on table "gtemp_pk_rel" violates foreign key constraint "tmp2_a_fkey" on table "tmp2"
+DETAIL:  Key (a)=(1) is still referenced from table "tmp2".
+ALTER TABLE tmp2 DROP CONSTRAINT tmp2_a_fkey;
+ALTER TABLE tmp2 ADD FOREIGN KEY (a) REFERENCES gtemp_pk_rel ON DELETE CASCADE;
+DELETE FROM gtemp_pk_rel WHERE a = 1;
+SELECT * FROM tmp2;
+ a 
+---
+(0 rows)
+
+DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
 -- Test ALTER TABLE ... SET TABLESPACE -- reltablespace changes locally and globally
 CREATE GLOBAL TEMP TABLE tmp2 (a int);
 INSERT INTO tmp2 VALUES (1);
@@ -261,68 +443,68 @@ DETAIL:  tablespace for table tmp2
 DROP TABLE tmp2;
 DROP TABLESPACE regress_temp_test_tablespace;
 -- Test TRUNCATE
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
 BEGIN;
 TRUNCATE tmp1;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 ROLLBACK;
 SELECT * FROM tmp1;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 BEGIN;
 SAVEPOINT sp1;
 TRUNCATE tmp1;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 RELEASE sp1;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 ROLLBACK;
 SELECT * FROM tmp1;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 BEGIN;
 SAVEPOINT sp1;
 TRUNCATE tmp1;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 ROLLBACK TO sp1;
 SELECT * FROM tmp1;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 COMMIT;
 SELECT * FROM tmp1;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 TRUNCATE tmp1;
 SELECT * FROM tmp1;
- a 
----
+ a | b 
+---+---
 (0 rows)
 
 -- Test REPACK -- relfilenode only changes locally
@@ -353,6 +535,20 @@ SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'chan
  unchanged          | changed
 (1 row)
 
+-- Test CLUSTER -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass \gset
+CLUSTER tmp1 USING tmp1_pkey;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
 -- Test subtransaction rollback of DROP
 \c
 SET search_path = global_temp_tests;
@@ -366,35 +562,71 @@ SELECT count(*) FROM tmp1;
 SAVEPOINT sp;
 DROP TABLE tmp1;
 ROLLBACK TO sp;
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
 COMMIT;
 -- Re-check pg_gtrs_in_use()
 SELECT c.relname,
        t.relfilenode = c.relfilenode,
        t.reltablespace = c.reltablespace
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
+ WHERE c.relname !~ 'pg_toast_'
  ORDER BY c.relname;
- relname | ?column? | ?column? 
----------+----------+----------
- tmp1    | t        | t
-(1 row)
+  relname  | ?column? | ?column? 
+-----------+----------+----------
+ tmp1      | t        | t
+ tmp1_pkey | t        | t
+(2 rows)
 
 -- Test view creation
 CREATE VIEW v AS SELECT * FROM tmp1;
 SELECT * FROM v;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 DROP VIEW v;
 CREATE TEMP VIEW v AS SELECT * FROM tmp1;
 SELECT * FROM v;
- a 
----
- 1
+ a |  b  
+---+-----
+ 1 | xxx
 (1 row)
 
 DROP VIEW v;
 CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
 ERROR:  views cannot be global temporary because they do not have storage
+-- VACUUM initializes toast tables
+\c
+SET search_path = global_temp_tests;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+       EXISTS (SELECT 1 FROM pg_gtrs_in_use() t WHERE t.oid = c.reltoastrelid) AS used
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+ exists | used 
+--------+------
+ t      | f
+(1 row)
+
+VACUUM tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+       EXISTS (SELECT 1 FROM pg_gtrs_in_use() t WHERE t.oid = c.reltoastrelid) AS used
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+ exists | used 
+--------+------
+ t      | t
+(1 row)
+
+\c
+SET search_path = global_temp_tests;
+VACUUM FULL tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+       EXISTS (SELECT 1 FROM pg_gtrs_in_use() t WHERE t.oid = c.reltoastrelid) AS used
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+ exists | used 
+--------+------
+ t      | t
+(1 row)
+
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 6fe921a5139..e6d386b2b52 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -11,7 +11,7 @@ SET ROLE regress_global_temp_user;
 
 -- Test table creation
 CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
-CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text);
 CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
 CREATE SCHEMA global_temp_yyy;
 CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
@@ -29,7 +29,7 @@ DROP SCHEMA global_temp_xxx CASCADE;
 DROP SCHEMA global_temp_yyy CASCADE;
 
 -- Basic tests
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
 SELECT * FROM tmp1;
 \c
 SET search_path = global_temp_tests;
@@ -56,6 +56,36 @@ SELECT c.relname,
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
 
+-- Test index
+INSERT INTO tmp1 VALUES (1, 'xxx');
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE a = 1;
+SELECT * FROM tmp1 WHERE a = 1;
+RESET enable_seqscan;
+
+-- Test concurrent index build -- CONCURRENTLY is ignored with temp tables
+CREATE INDEX CONCURRENTLY tmp1_b_idx ON tmp1(b);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE b = 'xxx';
+SELECT * FROM tmp1 WHERE b = 'xxx';
+RESET enable_seqscan;
+REINDEX INDEX CONCURRENTLY tmp1_b_idx;
+REINDEX TABLE CONCURRENTLY tmp1;
+
+-- Test REINDEX -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1_b_idx'::regclass \gset
+
+REINDEX INDEX tmp1_b_idx;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1_b_idx'::regclass;
+DROP INDEX CONCURRENTLY tmp1_b_idx;
+
 -- Test ON COMMIT DELETE ROWS
 CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
 BEGIN;
@@ -103,6 +133,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,
+       pg_gtr_index_is_valid(c.oid) AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ 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,
+       pg_gtr_index_is_valid(c.oid) AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ 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,
+       pg_gtr_index_is_valid(c.oid) AS local_valid
+  FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ 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);
@@ -113,9 +177,21 @@ DROP TABLE tmp2;
 -- Test foreign keys
 CREATE TABLE perm_pk_rel (a int PRIMARY KEY);
 CREATE TEMP TABLE temp_pk_rel (a int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE gtemp_pk_rel (a int PRIMARY KEY);
+CREATE TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
+CREATE TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
 CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES perm_pk_rel); -- fail
 CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES temp_pk_rel); -- fail
-DROP TABLE perm_pk_rel, temp_pk_rel;
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel);
+INSERT INTO gtemp_pk_rel VALUES (1);
+INSERT INTO tmp2 VALUES (1);
+INSERT INTO tmp2 VALUES (2); -- fail
+DELETE FROM gtemp_pk_rel WHERE a = 1; -- fail
+ALTER TABLE tmp2 DROP CONSTRAINT tmp2_a_fkey;
+ALTER TABLE tmp2 ADD FOREIGN KEY (a) REFERENCES gtemp_pk_rel ON DELETE CASCADE;
+DELETE FROM gtemp_pk_rel WHERE a = 1;
+SELECT * FROM tmp2;
+DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
 
 -- Test ALTER TABLE ... SET TABLESPACE -- reltablespace changes locally and globally
 CREATE GLOBAL TEMP TABLE tmp2 (a int);
@@ -159,7 +235,7 @@ DROP TABLE tmp2;
 DROP TABLESPACE regress_temp_test_tablespace;
 
 -- Test TRUNCATE
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
 BEGIN;
 TRUNCATE tmp1;
 SELECT * FROM tmp1;
@@ -209,6 +285,17 @@ SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'chan
   FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
  WHERE c.oid = 'tmp1'::regclass;
 
+-- Test CLUSTER -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass \gset
+
+CLUSTER tmp1 USING tmp1_pkey;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+
 -- Test subtransaction rollback of DROP
 \c
 SET search_path = global_temp_tests;
@@ -217,7 +304,7 @@ SELECT count(*) FROM tmp1;
 SAVEPOINT sp;
 DROP TABLE tmp1;
 ROLLBACK TO sp;
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
 COMMIT;
 
 -- Re-check pg_gtrs_in_use()
@@ -225,6 +312,7 @@ SELECT c.relname,
        t.relfilenode = c.relfilenode,
        t.reltablespace = c.reltablespace
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
+ WHERE c.relname !~ 'pg_toast_'
  ORDER BY c.relname;
 
 -- Test view creation
@@ -237,3 +325,25 @@ SELECT * FROM v;
 DROP VIEW v;
 
 CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
+
+-- VACUUM initializes toast tables
+\c
+SET search_path = global_temp_tests;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+       EXISTS (SELECT 1 FROM pg_gtrs_in_use() t WHERE t.oid = c.reltoastrelid) AS used
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+
+VACUUM tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+       EXISTS (SELECT 1 FROM pg_gtrs_in_use() t WHERE t.oid = c.reltoastrelid) AS used
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+
+\c
+SET search_path = global_temp_tests;
+VACUUM FULL tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+       EXISTS (SELECT 1 FROM pg_gtrs_in_use() t WHERE t.oid = c.reltoastrelid) AS used
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
-- 
2.51.0

