From 00a3b9b41fb8cc3764ccbe31ef7b4eb1b946c828 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Tue, 9 Jun 2026 11:14:27 +0100
Subject: [PATCH v1 1/9] Basic support for global temporary tables.

This allows global temporary tables to be created. When a global
temporary table is first accessed in a session, it is initialised by
creating local storage for it. All such storage created is tracked, in
case of rollback (in which case the table needs to be reinitialised),
and deleted on backend end.

All usage of global temporary tables is recorded in a shared hash
table to prevent certain operations like an ALTER TABLE rewrite, if
the table is being used by another session.

Lots more to do, to make a fully-baked feature...
---
 src/backend/access/heap/heapam_handler.c      |    5 +-
 src/backend/access/transam/xact.c             |   26 +
 src/backend/catalog/Makefile                  |    1 +
 src/backend/catalog/catalog.c                 |    1 +
 src/backend/catalog/global_temp.c             | 1205 +++++++++++++++++
 src/backend/catalog/heap.c                    |   21 +-
 src/backend/catalog/information_schema.sql    |    1 +
 src/backend/catalog/meson.build               |    1 +
 src/backend/catalog/namespace.c               |   15 +-
 src/backend/catalog/pg_publication.c          |    3 +-
 src/backend/catalog/storage.c                 |   17 +-
 src/backend/commands/dbcommands.c             |    9 +-
 src/backend/commands/lockcmds.c               |    3 +-
 src/backend/commands/propgraphcmds.c          |    5 +
 src/backend/commands/repack.c                 |   10 +
 src/backend/commands/tablecmds.c              |   88 +-
 src/backend/commands/tablespace.c             |    3 +-
 src/backend/commands/view.c                   |    9 +
 src/backend/optimizer/path/allpaths.c         |    6 +-
 src/backend/parser/gram.y                     |   38 +-
 src/backend/postmaster/autovacuum.c           |   16 +-
 src/backend/postmaster/datachecksum_state.c   |   23 +
 src/backend/storage/buffer/bufmgr.c           |   36 +-
 .../utils/activity/wait_event_names.txt       |    3 +
 src/backend/utils/adt/dbsize.c                |    3 +
 src/backend/utils/cache/relcache.c            |   52 +-
 src/backend/utils/cache/relfilenumbermap.c    |    3 +-
 src/bin/pg_amcheck/pg_amcheck.c               |   19 +-
 src/bin/pg_dump/pg_dump.c                     |    8 +-
 src/bin/pg_upgrade/info.c                     |    3 +
 src/bin/psql/describe.c                       |   22 +-
 src/bin/scripts/vacuuming.c                   |    4 +-
 src/include/catalog/global_temp.h             |   42 +
 src/include/catalog/pg_class.h                |    3 +-
 src/include/catalog/storage.h                 |    2 +-
 src/include/storage/lwlocklist.h              |    3 +
 src/include/storage/subsystemlist.h           |    3 +
 src/include/utils/rel.h                       |   23 +-
 src/test/isolation/expected/global-temp.out   |  242 ++++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/global-temp.spec     |   56 +
 .../test_checksums/t/010_global_temp.pl       |   56 +
 src/test/regress/expected/alter_table.out     |    6 +-
 .../expected/create_property_graph.out        |    2 +
 src/test/regress/expected/create_table.out    |   10 +-
 src/test/regress/expected/create_view.out     |    2 +-
 src/test/regress/expected/global_temp.out     |  273 ++++
 src/test/regress/expected/type_sanity.out     |    2 +-
 src/test/regress/parallel_schedule            |    4 +-
 src/test/regress/sql/alter_table.sql          |    5 +-
 .../regress/sql/create_property_graph.sql     |    1 +
 src/test/regress/sql/create_table.sql         |    6 +
 src/test/regress/sql/global_temp.sql          |  151 +++
 src/test/regress/sql/type_sanity.sql          |    2 +-
 src/tools/pgindent/typedefs.list              |    5 +
 55 files changed, 2457 insertions(+), 102 deletions(-)
 create mode 100644 src/backend/catalog/global_temp.c
 create mode 100644 src/include/catalog/global_temp.h
 create mode 100644 src/test/isolation/expected/global-temp.out
 create mode 100644 src/test/isolation/specs/global-temp.spec
 create mode 100644 src/test/modules/test_checksums/t/010_global_temp.pl
 create mode 100644 src/test/regress/expected/global_temp.out
 create mode 100644 src/test/regress/sql/global_temp.sql

diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 2268cc277bc..d9f64287c59 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -514,7 +514,7 @@ heapam_relation_set_new_filelocator(Relation rel,
 	 */
 	*minmulti = GetOldestMultiXactId();
 
-	srel = RelationCreateStorage(*newrlocator, persistence, true);
+	srel = RelationCreateStorage(rel->rd_id, *newrlocator, persistence, true);
 
 	/*
 	 * If required, set up an init fork for an unlogged table so that it can
@@ -557,7 +557,8 @@ heapam_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
 	 * NOTE: any conflict in relfilenumber value will be caught in
 	 * RelationCreateStorage().
 	 */
-	dstrel = RelationCreateStorage(*newrlocator, rel->rd_rel->relpersistence, true);
+	dstrel = RelationCreateStorage(rel->rd_id, *newrlocator,
+								   rel->rd_rel->relpersistence, true);
 
 	/* copy main fork */
 	RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM,
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 5586fbe5b07..5d1b3555f12 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -32,6 +32,7 @@
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "access/xlogwait.h"
+#include "catalog/global_temp.h"
 #include "catalog/index.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_enum.h"
@@ -2352,6 +2353,13 @@ CommitTransaction(void)
 	 */
 	PreCommit_on_commit_actions();
 
+	/*
+	 * Process dropped global temporary relations, deleting their local
+	 * storage.  It makes sense to do this before smgrDoPendingSyncs() so that
+	 * we don't bother synchronizing these relations.
+	 */
+	PreCommit_GlobalTempRelation();
+
 	/*
 	 * Synchronize files that are created and not WAL-logged during this
 	 * transaction. This must happen before AtEOXact_RelationMap(), so that we
@@ -2462,6 +2470,9 @@ CommitTransaction(void)
 	/* Clean up the relation cache */
 	AtEOXact_RelationCache(true);
 
+	/* Clean up storage and usage records for global temporary relations */
+	AtEOXact_GlobalTempRelation(true);
+
 	/* Clean up the type cache */
 	AtEOXact_TypeCache();
 
@@ -2614,6 +2625,13 @@ PrepareTransaction(void)
 	 */
 	PreCommit_on_commit_actions();
 
+	/*
+	 * Process dropped global temporary relations, deleting their local
+	 * storage.  It makes sense to do this before smgrDoPendingSyncs() so that
+	 * we don't bother synchronizing these relations.
+	 */
+	PreCommit_GlobalTempRelation();
+
 	/*
 	 * Synchronize files that are created and not WAL-logged during this
 	 * transaction. This must happen before EndPrepare(), so that we don't see
@@ -2770,6 +2788,9 @@ PrepareTransaction(void)
 	/* Clean up the relation cache */
 	AtEOXact_RelationCache(true);
 
+	/* Clean up storage and usage records for global temporary relations */
+	AtEOXact_GlobalTempRelation(true);
+
 	/* Clean up the type cache */
 	AtEOXact_TypeCache();
 
@@ -3019,6 +3040,7 @@ AbortTransaction(void)
 		AtEOXact_Aio(false);
 		AtEOXact_Buffers(false);
 		AtEOXact_RelationCache(false);
+		AtEOXact_GlobalTempRelation(false);
 		AtEOXact_TypeCache();
 		AtEOXact_Inval(false);
 		AtEOXact_MultiXact();
@@ -5211,6 +5233,8 @@ CommitSubTransaction(void)
 						 true, false);
 	AtEOSubXact_RelationCache(true, s->subTransactionId,
 							  s->parent->subTransactionId);
+	AtEOSubXact_GlobalTempRelation(true, s->subTransactionId,
+								   s->parent->subTransactionId);
 	AtEOSubXact_TypeCache();
 	AtEOSubXact_Inval(true);
 	AtSubCommit_smgr();
@@ -5396,6 +5420,8 @@ AbortSubTransaction(void)
 		AtEOXact_Aio(false);
 		AtEOSubXact_RelationCache(false, s->subTransactionId,
 								  s->parent->subTransactionId);
+		AtEOSubXact_GlobalTempRelation(false, s->subTransactionId,
+									   s->parent->subTransactionId);
 		AtEOSubXact_TypeCache();
 		AtEOSubXact_Inval(false);
 		ResourceOwnerRelease(s->curTransactionOwner,
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 26fa0c9b18c..0fb085fd8ee 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	aclchk.o \
 	catalog.o \
 	dependency.o \
+	global_temp.o \
 	heap.o \
 	index.o \
 	indexing.o \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 7be49032934..fed69d36939 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -571,6 +571,7 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence)
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			procNumber = ProcNumberForTempRelations();
 			break;
 		case RELPERSISTENCE_UNLOGGED:
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
new file mode 100644
index 00000000000..a3ee3d424ca
--- /dev/null
+++ b/src/backend/catalog/global_temp.c
@@ -0,0 +1,1205 @@
+/*-------------------------------------------------------------------------
+ *
+ * global_temp.c
+ *	  Global temporary relation management.
+ *
+ * This tracks all global temporary relations in use across all backends,
+ * as well as any local storage created for global temporary relations used
+ * in this backend.
+ *
+ * When a global temporary relation is created or first opened, it is
+ * initialized for use, which (for most relkinds) includes creating local
+ * storage for it.  All global temporary relations in use and all local
+ * storage created is tracked, taking into account (sub)transaction
+ * rollback --- a rollback can undo the effects of creating or opening a
+ * global temporary relation, including the creation of local storage.  If
+ * a global temporary relation is first opened in a (sub)transaction which
+ * is then rolled back, it is reinitialized the next time it is opened.
+ * When the backend exits, all locally created storage is deleted.
+ *
+ * Relcache invalidation messages are passed on to code here so that it can
+ * deal with other backends dropping global temporary relations.  If a
+ * global temporary relation in use by this backed is dropped by another
+ * backend, all local storage created for the relation in this backend is
+ * deleted the next time a transaction commits.  (It could perhaps be done
+ * sooner, but it's not crucial, since such storage will be deleted on
+ * backend exit anyway.)
+ *
+ * A shared hash table is used to track all global temporary relations in
+ * use across all databases and all backends.  A "usage count" is kept for
+ * each relation, which is a count of the number of backends using it.
+ * This is used to prevent operations like ALTER TABLE from altering a
+ * global temporary table in a way that would require rewriting its
+ * contents, if it's in use by other backends, which cannot be allowed,
+ * since there is no way to rewrite the local storage of other backends.
+ *
+ * A global temporary relation is regarded as "in use" by a backend from
+ * the time it is created or first opened until the time it is dropped or
+ * the backend exits (or a rollback undoes the creation or opening of the
+ * relation).  This means that a backend that executes CREATE GLOBAL TEMP
+ * TABLE is counted as using it, even if it never opens it.
+ *
+ * When a global temporary relation is not in use by any backend, it has no
+ * physical storage.  Thus a global temporary relation must have a shared
+ * dependency on its tablespace to prevent the tablespace from being
+ * dropped while the relation is not being used.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/catalog/global_temp.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/tableam.h"
+#include "access/xact.h"
+#include "access/xlogutils.h"
+#include "catalog/global_temp.h"
+#include "catalog/storage.h"
+#include "lib/dshash.h"
+#include "miscadmin.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/relfilelocator.h"
+#include "storage/shmem.h"
+#include "storage/subsystems.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tuplestore.h"
+
+/*
+ * gtr_local_storage
+ *
+ *	Hash table to track local storage created by this backend for global
+ *	temporary relations.
+ */
+typedef struct GtrStorageEntry
+{
+	RelFileLocator rlocator;	/* lookup key: relfilelocator of storage */
+	Oid			relid;			/* OID of relation owning the storage */
+	SubTransactionId created_subid; /* storage was created in current xact */
+	SubTransactionId dropped_subid; /* dropped with another subid set */
+} GtrStorageEntry;
+
+static HTAB *gtr_local_storage;
+
+/*
+ * eoxact_storage_list[]
+ *
+ *	List of relfilelocators of storage that (might) need AtEOXact cleanup
+ *	work.  As with the relcache's eoxact_list[], this list intentionally has
+ *	limited size, and we switch to a full hash table traversal if the list
+ *	overflows.
+ */
+#define MAX_EOXACT_STORAGE_LIST 32
+static RelFileLocator eoxact_storage_list[MAX_EOXACT_STORAGE_LIST];
+static int	eoxact_storage_list_len = 0;
+static bool eoxact_storage_list_overflowed = false;
+
+#define EOXactStorageListAdd(rlocator) \
+	do { \
+		if (eoxact_storage_list_len < MAX_EOXACT_STORAGE_LIST) \
+			eoxact_storage_list[eoxact_storage_list_len++] = (rlocator); \
+		else \
+			eoxact_storage_list_overflowed = true; \
+	} while (0)
+
+/*
+ * gtr_local_usage
+ *
+ *	Hash table to track global temporary relations in use in this backend.
+ */
+typedef struct GtrUsageEntry
+{
+	Oid			relid;			/* lookup key: OID of relation in use */
+	SubTransactionId started_subid; /* usage started in current xact */
+	SubTransactionId stopped_subid; /* usage ended with another subid set */
+} GtrUsageEntry;
+
+static HTAB *gtr_local_usage;
+
+/*
+ * eoxact_usage_list[]
+ *
+ *	List of OIDs of global temporary relation usage entries that (might) need
+ *	AtEOXact cleanup work.  Cf. eoxact_storage_list[].
+ */
+#define MAX_EOXACT_USAGE_LIST 32
+static Oid	eoxact_usage_list[MAX_EOXACT_USAGE_LIST];
+static int	eoxact_usage_list_len = 0;
+static bool eoxact_usage_list_overflowed = false;
+
+#define EOXactUsageListAdd(relid) \
+	do { \
+		if (eoxact_usage_list_len < MAX_EOXACT_USAGE_LIST) \
+			eoxact_usage_list[eoxact_usage_list_len++] = (relid); \
+		else \
+			eoxact_usage_list_overflowed = true; \
+	} while (0)
+
+/*
+ * Invalidation message handling lists:
+ *
+ *	gtrs_invalidated
+ *		OIDs of global temporary relations that we are using, for which we
+ *		have received an invalidation message.
+ *
+ *	gtrs_dropped
+ *		OIDs of global temporary relations that we were using, which have been
+ *		dropped (by us or another backend).
+ */
+static List *gtrs_invalidated;
+static List *gtrs_dropped;
+
+/*
+ * gtr_shared_usage
+ *
+ *	Shared hash table recording all global temporary relation usage across all
+ *	databases and backends.  For each relation, "usage_count" records the
+ *	number of backends (including us) using the relation.  Entries are
+ *	removed when the usage count hits zero.
+ */
+typedef struct GtrSharedUsageKey
+{
+	Oid			dbid;			/* DB containing global temporary relation */
+	Oid			relid;			/* OID of global temporary relation */
+} GtrSharedUsageKey;
+
+typedef struct GtrSharedUsageEntry
+{
+	GtrSharedUsageKey key;		/* lookup key: (dbid, relid) of relation */
+	int			usage_count;	/* number of backends accessing relation */
+} GtrSharedUsageEntry;
+
+static const dshash_parameters gtr_shared_usage_params = {
+	sizeof(GtrSharedUsageKey),
+	sizeof(GtrSharedUsageEntry),
+	dshash_memcmp,
+	dshash_memhash,
+	dshash_memcpy,
+	LWTRANCHE_GLOBAL_TEMP_REL_HASH
+};
+
+static dsa_area *gtr_shared_usage_dsa;
+static dshash_table *gtr_shared_usage;
+
+/*
+ * gtr_shmem_control
+ *
+ *	Shared memory state for the global temporary relation shared usage table.
+ */
+typedef struct GlobalTempRelShmemControl
+{
+	dsa_handle dsa_handle;		/* usage table's DSA handle */
+	dshash_table_handle dshash_handle;	/* usage table's dshash handle */
+} GlobalTempRelShmemControl;
+
+static GlobalTempRelShmemControl *gtr_shmem_control;
+
+/*
+ * GlobalTempRelShmemCallbacks
+ *
+ *	Callbacks to register our shared memory requirements and initialize it.
+ */
+static void
+gtr_shmem_request(void *arg)
+{
+	ShmemRequestStruct(.name = "Global Temporary Relation Usage Table",
+					   .size = sizeof(GlobalTempRelShmemControl),
+					   .ptr = (void **) &gtr_shmem_control,
+		);
+}
+
+static void
+gtr_shmem_init(void *arg)
+{
+	gtr_shmem_control->dsa_handle = DSA_HANDLE_INVALID;
+	gtr_shmem_control->dshash_handle = DSHASH_HANDLE_INVALID;
+}
+
+const ShmemCallbacks GlobalTempRelShmemCallbacks = {
+	.request_fn = gtr_shmem_request,
+	.init_fn = gtr_shmem_init,
+};
+
+/*
+ * gtr_delete_all_storage_on_exit
+ *
+ *	Backend exit callback to delete all local storage created for global
+ *	temporary relations in this backend.
+ */
+static void
+gtr_delete_all_storage_on_exit(int code, Datum arg)
+{
+	ProcNumber	backend;
+	HASH_SEQ_STATUS status;
+	GtrStorageEntry *entry;
+	SMgrRelation srel;
+
+	/* Loop over all storage created and delete it */
+	backend = ProcNumberForTempRelations();
+	hash_seq_init(&status, gtr_local_storage);
+	while ((entry = hash_seq_search(&status)) != NULL)
+	{
+		srel = smgropen(entry->rlocator, backend);
+		smgrdounlinkall(&srel, 1, false);
+		smgrclose(srel);
+	}
+}
+
+/*
+ * gtr_init_storage_table
+ *
+ *	Initialize the hash table recording local storage created for global
+ *	temporary relations, if not already done.
+ */
+static void
+gtr_init_storage_table(void)
+{
+	if (gtr_local_storage == NULL)
+	{
+		HASHCTL		ctl;
+
+		ctl.keysize = sizeof(RelFileLocator);
+		ctl.entrysize = sizeof(GtrStorageEntry);
+
+		gtr_local_storage =
+			hash_create("Global temporary relation storage table",
+						128, &ctl, HASH_ELEM | HASH_BLOBS);
+
+		/* Register callback to delete all local storage on exit */
+		before_shmem_exit(gtr_delete_all_storage_on_exit, 0);
+	}
+}
+
+/*
+ * gtr_storage_dropped
+ *
+ *	Invalidate a global temporary relation whose storage has been dropped.
+ *
+ *	This is called as part of AtEO(Sub)Xact cleanup if storage creation is
+ *	rolled back, or storage deletion is committed.  This can happen several
+ *	different ways:
+ *
+ *	- The relation was initialized in a transaction which was then rolled
+ *	  back, causing the local storage created during initialization to be
+ *	  dropped.
+ *
+ *	- An operation like REPACK or TRUNCATE committed and the old storage was
+ *	  dropped.
+ *
+ *	- An operation like REPACK or TRUNCATE was rolled back and the new storage
+ *	  was dropped.  The old storage may or may not still exist, depending on
+ *	  when it was created.
+ *
+ *	- The table itself was dropped.
+ *
+ *	Here, we have no way to distinguish between these cases, so we just mark
+ *	the relation's relcache entry as invalid (if it still has one), forcing it
+ *	to be reloaded and reinitialized when it is next opened.  New storage for
+ *	the relation will then be created, if needed.
+ */
+static void
+gtr_storage_dropped(Oid relid, RelFileLocator rlocator)
+{
+	/* If the relation is still in the relcache mark it as invalid */
+	RelationMarkInvalid(relid);
+
+	/*
+	 * Remove the hash entry for the dropped storage, forcing the relation to
+	 * create new storage if its relfilenode points to this storage after it
+	 * is reloaded.
+	 */
+	(void) hash_search(gtr_local_storage, &rlocator, HASH_REMOVE, NULL);
+}
+
+/*
+ * AtEOXact_StorageCleanup
+ *
+ *	Clean up storage records for a single global temporary relation at
+ *	main-transaction commit or abort.
+ *
+ *	NB: this processing must be idempotent, because EOXactStorageListAdd()
+ *	doesn't bother to prevent duplicate entries in eoxact_storage_list[].
+ */
+static void
+AtEOXact_StorageCleanup(GtrStorageEntry *entry, bool isCommit)
+{
+	/*
+	 * If the storage no longer exists after this transaction ends, the global
+	 * temporary relation that was using it may no longer have any storage.
+	 * Mark the relation as invalid and remove the storage hash entry, forcing
+	 * the relation to be reinitialized and have new storage created, if
+	 * necessary, when it is next loaded.  Otherwise, reset the hash entry's
+	 * subids to zero.
+	 */
+	if ((isCommit && entry->dropped_subid != InvalidSubTransactionId) ||
+		(!isCommit && entry->created_subid != InvalidSubTransactionId))
+	{
+		gtr_storage_dropped(entry->relid, entry->rlocator);
+	}
+	else
+	{
+		entry->created_subid = InvalidSubTransactionId;
+		entry->dropped_subid = InvalidSubTransactionId;
+	}
+}
+
+/*
+ * AtEOSubXact_StorageCleanup
+ *
+ *	Clean up storage records for a single global temporary relation at
+ *	subtransaction commit or abort.
+ *
+ *	NB: this processing must be idempotent, because EOXactStorageListAdd()
+ *	doesn't bother to prevent duplicate entries in eoxact_storage_list[].
+ */
+static void
+AtEOSubXact_StorageCleanup(GtrStorageEntry *entry, bool isCommit,
+						   SubTransactionId mySubid,
+						   SubTransactionId parentSubid)
+{
+	/*
+	 * Is it storage created in the current subtransaction?
+	 *
+	 * During subcommit, mark it as belonging to the parent, instead, as long
+	 * as it has not been deleted.  Otherwise, the global temporary relation
+	 * that was using this storage may no longer have any storage; mark the
+	 * relation as invalid and remove the storage hash entry, forcing the
+	 * relation to be reinitialized and have new storage created, if
+	 * necessary.
+	 */
+	if (entry->created_subid == mySubid)
+	{
+		Assert(entry->dropped_subid == mySubid ||
+			   entry->dropped_subid == InvalidSubTransactionId);
+
+		if (isCommit && entry->dropped_subid == InvalidSubTransactionId)
+			entry->created_subid = parentSubid;
+		else
+			gtr_storage_dropped(entry->relid, entry->rlocator);
+	}
+
+	/* Update the storage dropped subid */
+	if (entry->dropped_subid == mySubid)
+	{
+		if (isCommit)
+			entry->dropped_subid = parentSubid;
+		else
+			entry->dropped_subid = InvalidSubTransactionId;
+	}
+}
+
+/*
+ * gtr_remove_all_usage_on_exit
+ *
+ *	Backend exit callback to remove all records of this backend's use of
+ *	global temporary relations from the shared usage hash table.
+ */
+static void
+gtr_remove_all_usage_on_exit(int code, Datum arg)
+{
+	HASH_SEQ_STATUS status;
+	GtrUsageEntry *local_entry;
+	GtrSharedUsageKey key;
+	GtrSharedUsageEntry *shared_entry;
+
+	/* Loop over all the global temporary relations we were using */
+	hash_seq_init(&status, gtr_local_usage);
+	while ((local_entry = hash_seq_search(&status)) != NULL)
+	{
+		/* Find the shared usage entry */
+		key.dbid = MyDatabaseId;
+		key.relid = local_entry->relid;
+		shared_entry = dshash_find(gtr_shared_usage, &key, true);
+
+		if (shared_entry->usage_count > 1)
+		{
+			/* Other backends are still using the relation */
+			shared_entry->usage_count--;
+			dshash_release_lock(gtr_shared_usage, shared_entry);
+		}
+		else
+		{
+			/* No more backends using it */
+			dshash_delete_entry(gtr_shared_usage, shared_entry);
+		}
+
+		/*
+		 * NB: It is possible for gtr_remove_usage() to run after this, so we
+		 * need to remove all local entries too.
+		 */
+		(void) hash_search(gtr_local_usage,
+						   &local_entry->relid, HASH_REMOVE, NULL);
+	}
+}
+
+/*
+ * gtr_init_usage_tables
+ *
+ *	Initialize the local and shared usage hash tables for global temporary
+ *	relations, if not already done.
+ */
+static void
+gtr_init_usage_tables(void)
+{
+	HASHCTL		ctl;
+	MemoryContext oldcontext;
+
+	/* Local usage table */
+	if (gtr_local_usage == NULL)
+	{
+		ctl.keysize = sizeof(Oid);
+		ctl.entrysize = sizeof(GtrUsageEntry);
+
+		gtr_local_usage = hash_create("Global temporary relations in use locally",
+									  128, &ctl, HASH_ELEM | HASH_BLOBS);
+	}
+
+	/* Shared usage table */
+	if (gtr_shared_usage == NULL)
+	{
+		/* Use a lock to ensure only one process creates the table */
+		LWLockAcquire(GlobalTempRelControlLock, LW_EXCLUSIVE);
+
+		/* Be sure any local memory allocated by DSA routines is persistent */
+		oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+		if (gtr_shmem_control->dshash_handle == DSA_HANDLE_INVALID)
+		{
+			/* Initialize dynamic shared hash table to track shared usage */
+			gtr_shared_usage_dsa = dsa_create(LWTRANCHE_GLOBAL_TEMP_REL_DSA);
+			dsa_pin(gtr_shared_usage_dsa);
+			dsa_pin_mapping(gtr_shared_usage_dsa);
+
+			gtr_shared_usage = dshash_create(gtr_shared_usage_dsa,
+											 &gtr_shared_usage_params, NULL);
+
+			/* Store handles in shared memory for other backends to use */
+			gtr_shmem_control->dsa_handle = dsa_get_handle(gtr_shared_usage_dsa);
+			gtr_shmem_control->dshash_handle =
+				dshash_get_hash_table_handle(gtr_shared_usage);
+		}
+		else
+		{
+			/* Attach to existing dynamic shared hash table */
+			gtr_shared_usage_dsa = dsa_attach(gtr_shmem_control->dsa_handle);
+			dsa_pin_mapping(gtr_shared_usage_dsa);
+
+			gtr_shared_usage = dshash_attach(gtr_shared_usage_dsa,
+											 &gtr_shared_usage_params,
+											 gtr_shmem_control->dshash_handle,
+											 NULL);
+		}
+
+		MemoryContextSwitchTo(oldcontext);
+		LWLockRelease(GlobalTempRelControlLock);
+
+		/* Register callback to remove all our usage records on exit */
+		before_shmem_exit(gtr_remove_all_usage_on_exit, 0);
+	}
+}
+
+/*
+ * gtr_record_usage
+ *
+ *	Record the fact that we're using a global temporary relation by adding
+ *	entries to the local and shared usage hash tables.
+ *
+ *	Note: This is intentionally idempotent.
+ */
+static void
+gtr_record_usage(Oid relid)
+{
+	GtrUsageEntry *local_entry;
+	GtrSharedUsageKey key;
+	GtrSharedUsageEntry *shared_entry;
+	bool		found;
+
+	/* Initialize the usage tables, if necessary */
+	gtr_init_usage_tables();
+
+	/* Add local usage entry, if not already there */
+	local_entry = hash_search(gtr_local_usage, &relid, HASH_ENTER, &found);
+	if (found)
+		return;					/* already recorded, nothing to do */
+
+	/* Record the usage as starting in the current subtransaction */
+	local_entry->started_subid = GetCurrentSubTransactionId();
+	local_entry->stopped_subid = InvalidSubTransactionId;
+
+	/* Flag the usage entry for eoxact cleanup */
+	EOXactUsageListAdd(relid);
+
+	/* Add/update shared usage entry */
+	key.dbid = MyDatabaseId;
+	key.relid = relid;
+	shared_entry = dshash_find_or_insert(gtr_shared_usage, &key, &found);
+
+	if (found)
+		shared_entry->usage_count++;
+	else
+		shared_entry->usage_count = 1;
+
+	dshash_release_lock(gtr_shared_usage, shared_entry);
+}
+
+/*
+ * gtr_remove_usage
+ *
+ *	Remove our usage records for a global temporary relation that we're no
+ *	longer using.
+ *
+ *	Note: This is intentionally idempotent.
+ */
+static void
+gtr_remove_usage(Oid relid)
+{
+	GtrSharedUsageKey key;
+	GtrSharedUsageEntry *shared_entry;
+
+	/* Initialize the usage tables, if necessary */
+	gtr_init_usage_tables();
+
+	/* Remove local usage entry */
+	if (!hash_search(gtr_local_usage, &relid, HASH_REMOVE, NULL))
+		return;					/* nothing to do */
+
+	/* Update/delete shared usage entry */
+	key.dbid = MyDatabaseId;
+	key.relid = relid;
+	shared_entry = dshash_find(gtr_shared_usage, &key, true);
+
+	if (shared_entry->usage_count > 1)
+	{
+		/* Other backends are still using the relation */
+		shared_entry->usage_count--;
+		dshash_release_lock(gtr_shared_usage, shared_entry);
+	}
+	else
+	{
+		/* No more backends using it */
+		dshash_delete_entry(gtr_shared_usage, shared_entry);
+	}
+}
+
+/*
+ * AtEOXact_UsageCleanup
+ *
+ *	Clean up usage records for a single global temporary relation at
+ *	main-transaction commit or abort.
+ *
+ *	NB: this processing must be idempotent, because EOXactUsageListAdd()
+ *	doesn't bother to prevent duplicate entries in eoxact_usage_list[].
+ */
+static void
+AtEOXact_UsageCleanup(GtrUsageEntry *entry, bool isCommit)
+{
+	/*
+	 * If the relation is no longer in use after this transaction ends, remove
+	 * the usage hash table entries for it.  Otherwise, reset the hash entry's
+	 * subids to zero.
+	 */
+	if ((isCommit && entry->stopped_subid != InvalidSubTransactionId) ||
+		(!isCommit && entry->started_subid != InvalidSubTransactionId))
+	{
+		gtr_remove_usage(entry->relid);
+	}
+	else
+	{
+		entry->started_subid = InvalidSubTransactionId;
+		entry->stopped_subid = InvalidSubTransactionId;
+	}
+}
+
+/*
+ * AtEOSubXact_UsageCleanup
+ *
+ *	Clean up usage records for a single global temporary relation at
+ *	subtransaction commit or abort.
+ *
+ *	NB: this processing must be idempotent, because EOXactUsageListAdd()
+ *	doesn't bother to prevent duplicate entries in eoxact_usage_list[].
+ */
+static void
+AtEOSubXact_UsageCleanup(GtrUsageEntry *entry, bool isCommit,
+						 SubTransactionId mySubid,
+						 SubTransactionId parentSubid)
+{
+	/*
+	 * Did usage start in the current subtransaction?
+	 *
+	 * During subcommit, mark it as starting in the parent, instead, as long
+	 * as it has not been stopped.  Otherwise, the global temporary relation
+	 * is no longer in use.
+	 */
+	if (entry->started_subid == mySubid)
+	{
+		Assert(entry->stopped_subid == mySubid ||
+			   entry->stopped_subid == InvalidSubTransactionId);
+
+		if (isCommit && entry->stopped_subid == InvalidSubTransactionId)
+			entry->started_subid = parentSubid;
+		else
+			gtr_remove_usage(entry->relid);
+	}
+
+	/* Update the usage stopped subid */
+	if (entry->stopped_subid == mySubid)
+	{
+		if (isCommit)
+			entry->stopped_subid = parentSubid;
+		else
+			entry->stopped_subid = InvalidSubTransactionId;
+	}
+}
+
+/*
+ * gtr_process_invalidated_gtrs
+ *
+ *	Process the list of invalidated global temporary relations and work out
+ *	which relations have been dropped.  Note that this will include locally
+ *	dropped relations as well as relations dropped by other backends.
+ */
+static void
+gtr_process_invalidated_gtrs(void)
+{
+	MemoryContext oldcontext;
+	Oid			relid;
+
+	/*
+	 * Scan the gtrs_invalidated list and add any dropped relations to the
+	 * gtrs_dropped list.  Since the transaction might fail later on, we need
+	 * the gtrs_dropped list to persist until we can successfully process it.
+	 */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	/*
+	 * As we scan the gtrs_invalidated list, more invalidation messages might
+	 * arrive, so keep going until it is empty.
+	 */
+	while (gtrs_invalidated != NIL)
+	{
+		/* Pop the last relid from the list */
+		relid = llast_oid(gtrs_invalidated);
+		gtrs_invalidated = list_delete_last(gtrs_invalidated);
+
+		/* Ignore relations we've already found */
+		if (list_member_oid(gtrs_dropped, relid))
+			continue;
+
+		/* Ignore relations that still exist */
+		if (SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			continue;
+
+		/* Relation dropped; add it to the gtrs_dropped list */
+		gtrs_dropped = lappend_oid(gtrs_dropped, relid);
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * TrackGlobalTempRelationStorage
+ *
+ *	Track about-to-be-created or scheduled-to-be-deleted storage for a global
+ *	temporary relation, and arrange for all storage created to be deleted on
+ *	backend exit.
+ *
+ *	This is called for global temporary relations whenever storage is created
+ *	using RelationCreateStorage() or deleted using RelationDropStorage().
+ */
+void
+TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
+							   ProcNumber backend, bool create)
+{
+	GtrStorageEntry *entry;
+	bool		found;
+	SMgrRelation srel;
+
+	if (create)
+	{
+		/* Initialize the storage table, if necessary */
+		gtr_init_storage_table();
+
+		/* Insert an entry to track the storage */
+		entry = hash_search(gtr_local_storage, &rlocator, HASH_ENTER, &found);
+		if (found)
+			elog(ERROR, "Storage already exists for relation %u", relid);
+
+		/*
+		 * We're about to create storage for a global temporary relation.
+		 * First, check if storage already exists and if so, delete it --- can
+		 * happen if a previous backend with the same ProcNumber crashed, and
+		 * RemovePgTempFiles() didn't delete it.  The old storage is deleted
+		 * non-transactionally, so this is never rolled back.
+		 */
+		srel = smgropen(rlocator, backend);
+		if (smgrexists(srel, MAIN_FORKNUM))
+			smgrdounlinkall(&srel, 1, false);
+		smgrclose(srel);
+
+		/* Mark the storage as created in the current subtransaction */
+		entry->relid = relid;
+		entry->created_subid = GetCurrentSubTransactionId();
+		entry->dropped_subid = InvalidSubTransactionId;
+	}
+	else
+	{
+		/* Mark the storage as deleted in the current subtransaction */
+		entry = gtr_local_storage == NULL ? NULL :
+			hash_search(gtr_local_storage, &rlocator, HASH_FIND, NULL);
+		if (entry == NULL)
+			elog(ERROR, "Storage not found for relation %u", relid);
+
+		entry->dropped_subid = GetCurrentSubTransactionId();
+	}
+
+	/* Flag the storage for eoxact cleanup */
+	EOXactStorageListAdd(rlocator);
+}
+
+/*
+ * ReassignGlobalTempRelationStorage
+ *
+ *	Reassign global temporary relation storage to a different relation.  This
+ *	is needed for operations such as ALTER TABLE and REPACK, that rewrite a
+ *	relation's contents by building a transient relation and then swapping its
+ *	storage with the original relation.  We must mark the new storage as
+ *	belonging to the original relation here, otherwise it would be deleted
+ *	when the transient relation is dropped.
+ *
+ *	Note: we have no way of undoing this reassignment in case of rollback, so
+ *	we do not assign the original storage to the transient relation, since
+ *	that would leave it in an invalid state after rollback.  This isn't a
+ *	problem for the new storage, since that is dropped on rollback.  Thus,
+ *	this operates in the same way as TRUNCATE, where both the old and new
+ *	storage are temporarily marked as belonging to the same relation.  On
+ *	commit, the old storage is dropped and the relation is left pointing to
+ *	the new storage, and on rollback the new storage is dropped and the
+ *	relation is reloaded and made to point to the old storage.
+ */
+void
+ReassignGlobalTempRelationStorage(RelFileLocator rlocator,
+								  Oid newRelid)
+{
+	GtrStorageEntry *entry;
+
+	/* Must already be tracking the storage */
+	entry = hash_search(gtr_local_storage, &rlocator, HASH_FIND, NULL);
+	if (entry == NULL)
+		elog(ERROR, "could not find global temp relation storage {spcOid: %u, dbOid: %u, relNumber: %u}",
+			 rlocator.spcOid, rlocator.dbOid, rlocator.relNumber);
+
+	/* Reassign it */
+	entry->relid = newRelid;
+}
+
+/*
+ * InitGlobalTempRelation
+ *
+ *	Initialize a global temporary relation for use in this backend, if we
+ *	haven't already done so.
+ *
+ *	This is intended for global temporary relations that were created in some
+ *	other backend.  Such relations will have valid catalog entries, but may
+ *	have no local storage for this backend yet.
+ *
+ *	Note: This is intentionally idempotent.
+ */
+void
+InitGlobalTempRelation(Relation relation)
+{
+	/* Create storage for the relation, if it has none */
+	if (RELKIND_HAS_STORAGE(relation->rd_rel->relkind) &&
+		(gtr_local_storage == NULL ||
+		 hash_search(gtr_local_storage,
+					 &relation->rd_locator, HASH_FIND, NULL) == NULL))
+	{
+		/* Create (and track) storage for the relation */
+		if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
+			table_relation_set_new_filelocator(relation,
+											   &relation->rd_locator,
+											   relation->rd_rel->relpersistence,
+											   &relation->rd_rel->relfrozenxid,
+											   &relation->rd_rel->relminmxid);
+		else
+			RelationCreateStorage(relation->rd_id,
+								  relation->rd_locator,
+								  relation->rd_rel->relpersistence,
+								  true);
+	}
+
+	/* The remaining initialization works as if we had created it locally */
+	GlobalTempRelationCreated(relation);
+}
+
+/*
+ * InvalidateGlobalTempRelation
+ *
+ *	Process an invalidation message for a relation.
+ *
+ *	We are only interested in global temporary relations that we are currently
+ *	using, but the relcache will call this for all invalidated relations, not
+ *	just global temporary relations, since it has no way of knowing the
+ *	difference for relations no longer in its cache.  We filter out the ones
+ *	we're not interested in.
+ *
+ *	For a whole-relcache invalidation, RelationCacheInvalidate() will invoke
+ *	this with relid = InvalidOid.
+ */
+void
+InvalidateGlobalTempRelation(Oid relid)
+{
+	MemoryContext oldcontext;
+	HASH_SEQ_STATUS status;
+	GtrUsageEntry *entry;
+
+	/* Quick exit if we haven't used any global temporary relations */
+	if (gtr_local_usage == NULL)
+		return;
+
+	/* Be sure any memory allocated for gtrs_invalidated is persistent */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	/*
+	 * We can't do any DB access here, so just make a record of the
+	 * invalidations that might be of interest to us (those for in-use global
+	 * temporary relations).  We don't care about global temporary relations
+	 * that we haven't touched, or any other types of relations.
+	 */
+	if (OidIsValid(relid))
+	{
+		/* Invalidate rel if it's a locally in-use global temp relation */
+		if (hash_search(gtr_local_usage, &relid, HASH_FIND, NULL))
+		{
+			gtrs_invalidated = list_append_unique_oid(gtrs_invalidated, relid);
+		}
+	}
+	else
+	{
+		/* Invalidate all global temporary relations in use locally */
+		list_free(gtrs_invalidated);
+		gtrs_invalidated = NIL;
+
+		hash_seq_init(&status, gtr_local_usage);
+		while ((entry = hash_seq_search(&status)) != NULL)
+		{
+			gtrs_invalidated = lappend_oid(gtrs_invalidated, entry->relid);
+		}
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * GlobalTempRelationCreated
+ *
+ *	Initialize a just-created global temporary relation for use in this
+ *	backend.  This is also used by InitGlobalTempRelation() for just-opened
+ *	relations created in other backends, after we have created local storage
+ *	for them.
+ *
+ *	Note: This is intentionally idempotent.
+ */
+void
+GlobalTempRelationCreated(Relation relation)
+{
+	/* Record our use of the relation */
+	gtr_record_usage(relation->rd_id);
+}
+
+/*
+ * GlobalTempRelationDropped
+ *
+ *	Tidy up after a global temporary relation has been dropped.
+ *
+ *	Note: This is intentionally idempotent.  It is called for locally dropped
+ *	relations as well as relations that we were using that were dropped by
+ *	other backends, which means it can be called twice for the same relation.
+ */
+void
+GlobalTempRelationDropped(Oid relid)
+{
+	GtrUsageEntry *entry;
+
+	/*
+	 * Mark the relation's usage as ending in the current subtransaction, if
+	 * we haven't done so already.
+	 */
+	entry = hash_search(gtr_local_usage, &relid, HASH_FIND, NULL);
+
+	if (entry && entry->stopped_subid == InvalidSubTransactionId)
+	{
+		entry->stopped_subid = GetCurrentSubTransactionId();
+
+		/* Flag the usage entry for eoxact cleanup */
+		EOXactUsageListAdd(relid);
+	}
+}
+
+/*
+ * PreCommit_GlobalTempRelation
+ *
+ *	Process all dropped global temporary relations that we were using.  For
+ *	relations dropped by other backends, we need to delete any local storage
+ *	that we created.  For relations dropped by this backend, the storage
+ *	should have already been scheduled for deletion, so we ought to have
+ *	nothing to do.
+ */
+void
+PreCommit_GlobalTempRelation(void)
+{
+	HASH_SEQ_STATUS status;
+	GtrStorageEntry *storage_entry;
+	Relation	rel;
+
+	/*
+	 * Update the list of dropped global temporary relations from the list of
+	 * invalidated relations.
+	 */
+	gtr_process_invalidated_gtrs();
+
+	/* Quick exit if no global temporary relations were dropped */
+	if (gtrs_dropped == NIL)
+		return;
+
+	/*
+	 * Schedule all local storage that we created for dropped relations to be
+	 * deleted at transaction commit.  AtEOXact_GlobalTempRelation() will do
+	 * the remaining cleanup work for gtrs_dropped and the hash tables, if the
+	 * transaction commits successfully.  Otherwise, the gtrs_dropped list
+	 * will be untouched and this will be repeated the next time a transaction
+	 * commits.
+	 */
+	hash_seq_init(&status, gtr_local_storage);
+	while ((storage_entry = hash_seq_search(&status)) != NULL)
+	{
+		/* Ignore storage already scheduled to be deleted */
+		if (storage_entry->dropped_subid != InvalidSubTransactionId)
+			continue;
+
+		/* Ignore entries not in the dropped list */
+		if (!list_member_oid(gtrs_dropped, storage_entry->relid))
+			continue;
+
+		/* Need a faked-up Relation descriptor */
+		rel = CreateFakeRelcacheEntry(storage_entry->rlocator);
+		rel->rd_id = storage_entry->relid;
+		rel->rd_backend = ProcNumberForTempRelations();
+		rel->rd_smgr = NULL;
+		rel->rd_rel->relpersistence = RELPERSISTENCE_GLOBAL_TEMP;
+
+		/* Schedule the storage to be deleted and tidy up */
+		RelationDropStorage(rel);
+		FreeFakeRelcacheEntry(rel);
+	}
+
+	/*
+	 * Perform additional tidying up for all dropped relations.
+	 */
+	foreach_oid(relid, gtrs_dropped)
+	{
+		GlobalTempRelationDropped(relid);
+	}
+}
+
+/*
+ * AtEOXact_GlobalTempRelation
+ *
+ *	Clean up storage and usage records at main-transaction commit or abort.
+ */
+void
+AtEOXact_GlobalTempRelation(bool isCommit)
+{
+	HASH_SEQ_STATUS status;
+	GtrStorageEntry *storage_entry;
+	GtrUsageEntry *usage_entry;
+
+	/*
+	 * Unless the eoxact_storage_list[] overflowed, we only need to examine
+	 * the storage listed in it.  Otherwise fall back on a hash_seq_search
+	 * scan --- see similar code in AtEOXact_RelationCache().
+	 */
+	if (eoxact_storage_list_overflowed)
+	{
+		hash_seq_init(&status, gtr_local_storage);
+		while ((storage_entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOXact_StorageCleanup(storage_entry, isCommit);
+		}
+	}
+	else
+	{
+		for (int i = 0; i < eoxact_storage_list_len; i++)
+		{
+			storage_entry = hash_search(gtr_local_storage,
+										&eoxact_storage_list[i],
+										HASH_FIND, NULL);
+			if (storage_entry)
+				AtEOXact_StorageCleanup(storage_entry, isCommit);
+		}
+	}
+
+	/* Similarly, cleanup usage records */
+	if (eoxact_usage_list_overflowed)
+	{
+		hash_seq_init(&status, gtr_local_usage);
+		while ((usage_entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOXact_UsageCleanup(usage_entry, isCommit);
+		}
+	}
+	else
+	{
+		for (int i = 0; i < eoxact_usage_list_len; i++)
+		{
+			usage_entry = hash_search(gtr_local_usage, &eoxact_usage_list[i],
+									  HASH_FIND, NULL);
+			if (usage_entry)
+				AtEOXact_UsageCleanup(usage_entry, isCommit);
+		}
+	}
+
+	/* Now we're out of the transaction and can clear the lists */
+	eoxact_storage_list_len = 0;
+	eoxact_storage_list_overflowed = false;
+	list_free(gtrs_dropped);
+	gtrs_dropped = NIL;
+}
+
+/*
+ * AtEOSubXact_GlobalTempRelation
+ *
+ *	Clean up storage records at sub-transaction commit or abort.
+ */
+void
+AtEOSubXact_GlobalTempRelation(bool isCommit, SubTransactionId mySubid,
+							   SubTransactionId parentSubid)
+{
+	HASH_SEQ_STATUS status;
+	GtrStorageEntry *storage_entry;
+	GtrUsageEntry *usage_entry;
+
+	/*
+	 * Unless the eoxact_storage_list[] overflowed, we only need to examine
+	 * the storage listed in it.  Otherwise fall back on a hash_seq_search
+	 * scan.  Same logic as in AtEOXact_GlobalTempRelation().
+	 */
+	if (eoxact_storage_list_overflowed)
+	{
+		hash_seq_init(&status, gtr_local_storage);
+		while ((storage_entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOSubXact_StorageCleanup(storage_entry, isCommit, mySubid,
+									   parentSubid);
+		}
+	}
+	else
+	{
+		for (int i = 0; i < eoxact_storage_list_len; i++)
+		{
+			storage_entry = hash_search(gtr_local_storage,
+										&eoxact_storage_list[i],
+										HASH_FIND, NULL);
+			if (storage_entry)
+				AtEOSubXact_StorageCleanup(storage_entry, isCommit, mySubid,
+										   parentSubid);
+		}
+	}
+
+	/* Similarly, cleanup usage records */
+	if (eoxact_usage_list_overflowed)
+	{
+		hash_seq_init(&status, gtr_local_usage);
+		while ((usage_entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+									 parentSubid);
+		}
+	}
+	else
+	{
+		for (int i = 0; i < eoxact_usage_list_len; i++)
+		{
+			usage_entry = hash_search(gtr_local_usage, &eoxact_usage_list[i],
+									  HASH_FIND, NULL);
+			if (usage_entry)
+				AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+										 parentSubid);
+		}
+	}
+
+	/* Don't reset the lists; we still need more cleanup later */
+}
+
+/*
+ * IsOtherUsingGlobalTempRelation
+ *
+ *	Test if any other backend is using the specified global temporary
+ *	relation.  The caller should have an exclusive lock on the relation, or
+ *	else the result could be quickly out-dated.
+ */
+bool
+IsOtherUsingGlobalTempRelation(Oid relid)
+{
+	bool		used_locally;
+	GtrSharedUsageKey key;
+	GtrSharedUsageEntry *entry;
+	int			usage_count;
+
+	gtr_init_usage_tables();
+
+	/* Are we using the relation? (expect true) */
+	(void) hash_search(gtr_local_usage, &relid, HASH_FIND, &used_locally);
+
+	/* Total usage count (including us) */
+	key.dbid = MyDatabaseId;
+	key.relid = relid;
+	entry = dshash_find(gtr_shared_usage, &key, false);
+
+	if (entry)
+	{
+		usage_count = entry->usage_count;
+		Assert(usage_count > 0);
+		dshash_release_lock(gtr_shared_usage, entry);
+	}
+	else
+		usage_count = 0;
+
+	return used_locally ? (usage_count > 1) : (usage_count > 0);
+}
+
+/*
+ * AllGlobalTempRelationsInUse
+ *
+ *	Returns a list of OIDs of all global temporary relations in use (by any
+ *	backend) in the specified database (or all databases, if dbid is
+ *	InvalidOid).
+ *
+ *	Note: The result may be almost immediately out-dated.
+ */
+List *
+AllGlobalTempRelationsInUse(Oid dbid)
+{
+	List	   *rels_in_use = NIL;
+	dshash_seq_status status;
+	GtrSharedUsageEntry *entry;
+
+	gtr_init_usage_tables();
+
+	dshash_seq_init(&status, gtr_shared_usage, false);
+	while ((entry = dshash_seq_next(&status)) != NULL)
+	{
+		Assert(entry->usage_count > 0);
+		if (!OidIsValid(dbid) || entry->key.dbid == dbid)
+			rels_in_use = lappend_oid(rels_in_use, entry->key.relid);
+	}
+	dshash_seq_term(&status);
+
+	return rels_in_use;
+}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 88087654de9..2170b1eb935 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -36,6 +36,7 @@
 #include "access/tableam.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+#include "catalog/global_temp.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/objectaccess.h"
@@ -387,7 +388,8 @@ heap_create(const char *relname,
 											   relpersistence,
 											   relfrozenxid, relminmxid);
 		else if (RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
-			RelationCreateStorage(rel->rd_locator, relpersistence, true);
+			RelationCreateStorage(rel->rd_id, rel->rd_locator,
+								  relpersistence, true);
 		else
 			Assert(false);
 	}
@@ -396,8 +398,13 @@ heap_create(const char *relname,
 	 * If a tablespace is specified, removal of that tablespace is normally
 	 * protected by the existence of a physical file; but for relations with
 	 * no files, add a pg_shdepend entry to account for that.
+	 *
+	 * Note, however, that although global temporary relations may have files,
+	 * those files will go away at the end of the session, and so provide no
+	 * protection, and we must add a pg_shdepend entry in this case too.
 	 */
-	if (!create_storage && reltablespace != InvalidOid)
+	if ((!create_storage || relpersistence == RELPERSISTENCE_GLOBAL_TEMP) &&
+		reltablespace != InvalidOid)
 		recordDependencyOnTablespace(RelationRelationId, relid,
 									 reltablespace);
 
@@ -989,6 +996,10 @@ InsertPgClassTuple(Relation pg_class_desc,
 	CatalogTupleInsert(pg_class_desc, tup);
 
 	heap_freetuple(tup);
+
+	/* Additional setup for global temporary relations */
+	if (RELATION_IS_GLOBAL_TEMP(new_rel_desc))
+		GlobalTempRelationCreated(new_rel_desc);
 }
 
 /* --------------------------------
@@ -1597,6 +1608,7 @@ DeleteRelationTuple(Oid relid)
 {
 	Relation	pg_class_desc;
 	HeapTuple	tup;
+	char		relpersistence;
 
 	/* Grab an appropriate lock on the pg_class relation */
 	pg_class_desc = table_open(RelationRelationId, RowExclusiveLock);
@@ -1604,6 +1616,7 @@ DeleteRelationTuple(Oid relid)
 	tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "cache lookup failed for relation %u", relid);
+	relpersistence = ((Form_pg_class) GETSTRUCT(tup))->relpersistence;
 
 	/* delete the relation tuple from pg_class, and finish up */
 	CatalogTupleDelete(pg_class_desc, &tup->t_self);
@@ -1611,6 +1624,10 @@ DeleteRelationTuple(Oid relid)
 	ReleaseSysCache(tup);
 
 	table_close(pg_class_desc, RowExclusiveLock);
+
+	/* Additional tidying up for global temporary relations */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		GlobalTempRelationDropped(relid);
 }
 
 /*
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 4f0e2492937..a91a848cd17 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1952,6 +1952,7 @@ CREATE VIEW tables AS
 
            CAST(
              CASE WHEN nc.oid = pg_my_temp_schema() THEN 'LOCAL TEMPORARY'
+                  WHEN c.relpersistence = 'g' THEN 'GLOBAL TEMPORARY'
                   WHEN c.relkind IN ('r', 'p') THEN 'BASE TABLE'
                   WHEN c.relkind = 'v' THEN 'VIEW'
                   WHEN c.relkind = 'f' THEN 'FOREIGN'
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 11d21c5ad6b..7285ab2dfcf 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -4,6 +4,7 @@ backend_sources += files(
   'aclchk.c',
   'catalog.c',
   'dependency.c',
+  'global_temp.c',
   'heap.c',
   'index.c',
   'indexing.c',
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 56b87d878e8..b8555cdfa22 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -734,7 +734,8 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation)
  * to a no-longer-existent namespace.
  *
  * As a further side-effect, if the selected namespace is a temporary namespace,
- * we mark the RangeVar as RELPERSISTENCE_TEMP.
+ * we mark the RangeVar as RELPERSISTENCE_TEMP, unless it was marked as
+ * RELPERSISTENCE_GLOBAL_TEMP, in which case an error is raised.
  */
 Oid
 RangeVarGetAndCheckCreationNamespace(RangeVar *relation,
@@ -858,9 +859,19 @@ RangeVarAdjustRelationPersistence(RangeVar *newRelation, Oid nspid)
 				else
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-							 errmsg("cannot create temporary relation in non-temporary schema")));
+							 errmsg("cannot create local temporary relation in non-temporary schema")));
 			}
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			if (isTempOrTempToastNamespace(nspid))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot create global temporary relation in temporary schema")));
+			else if (isAnyTempNamespace(nspid))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot create relations in temporary schemas of other sessions")));
+			break;
 		case RELPERSISTENCE_PERMANENT:
 			if (isTempOrTempToastNamespace(nspid))
 				newRelation->relpersistence = RELPERSISTENCE_TEMP;
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 5c457d9aca8..ba21e9d3365 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -93,7 +93,8 @@ check_publication_add_relation(PublicationRelInfo *pri)
 				 errdetail("This operation is not supported for system tables.")));
 
 	/* UNLOGGED and TEMP relations cannot be part of publication. */
-	if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		targetrel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg(errormsg, relname),
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index e443a4993c5..c63788a3883 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
+#include "catalog/global_temp.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
 #include "miscadmin.h"
@@ -119,13 +120,16 @@ AddPendingSync(const RelFileLocator *rlocator)
  * pass register_delete = false.
  */
 SMgrRelation
-RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
+RelationCreateStorage(Oid relid, RelFileLocator rlocator, char relpersistence,
 					  bool register_delete)
 {
 	SMgrRelation srel;
 	ProcNumber	procNumber;
 	bool		needs_wal;
 
+	/* relid is only needed for global temporary relations */
+	Assert(OidIsValid(relid) || relpersistence != RELPERSISTENCE_GLOBAL_TEMP);
+
 	Assert(!IsInParallelMode());	/* couldn't update pendingSyncHash */
 
 	switch (relpersistence)
@@ -134,6 +138,12 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
 			procNumber = ProcNumberForTempRelations();
 			needs_wal = false;
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			/* Track storage created for global temporary relations */
+			procNumber = ProcNumberForTempRelations();
+			TrackGlobalTempRelationStorage(relid, rlocator, procNumber, true);
+			needs_wal = false;
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 			procNumber = INVALID_PROC_NUMBER;
 			needs_wal = false;
@@ -208,6 +218,11 @@ RelationDropStorage(Relation rel)
 {
 	PendingRelDelete *pending;
 
+	/* Track to-be-deleted storage for global temporary relations */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+		TrackGlobalTempRelationStorage(rel->rd_id, rel->rd_locator,
+									   rel->rd_backend, false);
+
 	/* Add the relation to the list of stuff to delete at commit */
 	pending = (PendingRelDelete *)
 		MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..52b86b01a22 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -410,11 +410,13 @@ ScanSourceDatabasePgClassTuple(HeapTupleData *tuple, Oid tbid, Oid dbid,
 	 * are inaccessible outside of the session that created them, which must
 	 * be gone already, and couldn't connect to a different database if it
 	 * still existed. autovacuum will eventually remove the pg_class entries
-	 * as well.
+	 * as well. Likewise, global temporary relations don't need to be copied,
+	 * though their pg_class entries never go away.
 	 */
 	if (classForm->reltablespace == GLOBALTABLESPACE_OID ||
 		!RELKIND_HAS_STORAGE(classForm->relkind) ||
-		classForm->relpersistence == RELPERSISTENCE_TEMP)
+		classForm->relpersistence == RELPERSISTENCE_TEMP ||
+		classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		return NULL;
 
 	/*
@@ -444,7 +446,8 @@ ScanSourceDatabasePgClassTuple(HeapTupleData *tuple, Oid tbid, Oid dbid,
 	relinfo->reloid = classForm->oid;
 
 	/* Temporary relations were rejected above. */
-	Assert(classForm->relpersistence != RELPERSISTENCE_TEMP);
+	Assert(classForm->relpersistence != RELPERSISTENCE_TEMP &&
+		   classForm->relpersistence != RELPERSISTENCE_GLOBAL_TEMP);
 	relinfo->permanent =
 		(classForm->relpersistence == RELPERSISTENCE_PERMANENT) ? true : false;
 
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index f66b8f17b9b..de5930ce60f 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -97,7 +97,8 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
 	 * transaction.
 	 */
 	relpersistence = get_rel_persistence(relid);
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP ||
+		relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE;
 
 	/* Check permissions. */
diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c
index cc516e27020..c2f560afcab 100644
--- a/src/backend/commands/propgraphcmds.c
+++ b/src/backend/commands/propgraphcmds.c
@@ -117,6 +117,11 @@ CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt)
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("property graphs cannot be unlogged because they do not have storage")));
 
+	if (stmt->pgname->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("property graphs cannot be global temporary because they do not have storage")));
+
 	components_persistence = RELPERSISTENCE_PERMANENT;
 
 	foreach(lc, stmt->vertex_tables)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 4d177c868bb..5b7a02cbd9f 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -43,6 +43,7 @@
 #include "access/xlog.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
+#include "catalog/global_temp.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/namespace.h"
@@ -1666,6 +1667,15 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 		rel2->rd_newRelfilelocatorSubid = rel1->rd_newRelfilelocatorSubid;
 		rel2->rd_firstRelfilelocatorSubid = rel1->rd_firstRelfilelocatorSubid;
 		RelationAssumeNewRelfilelocator(rel1);
+
+		/*
+		 * If they're global temp relations, reassign rel2's storage to rel1.
+		 * NB: We intentionally do not reassign rel1's storage to rel2, since
+		 * that would leave it in an invalid state on rollback.
+		 */
+		if (RELATION_IS_GLOBAL_TEMP(rel1))
+			ReassignGlobalTempRelationStorage(rel2->rd_locator, rel1->rd_id);
+
 		relation_close(rel1, NoLock);
 		relation_close(rel2, NoLock);
 	}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 265dcfe7fda..9be6a6d5a2b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -30,6 +30,7 @@
 #include "access/xlog.h"
 #include "access/xloginsert.h"
 #include "catalog/catalog.h"
+#include "catalog/global_temp.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/namespace.h"
@@ -175,6 +176,7 @@ typedef struct AlteredTableInfo
 	Oid			relid;			/* Relation to work on */
 	char		relkind;		/* Its relkind */
 	TupleDesc	oldDesc;		/* Pre-modification tuple descriptor */
+	char		oldrelpersistence;	/* Pre-modification relpersistence */
 
 	/*
 	 * Transiently set during Phase 2, normally set to NULL.
@@ -842,11 +844,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	/*
 	 * Check consistency of arguments
 	 */
-	if (stmt->oncommit != ONCOMMIT_NOOP
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+	if (stmt->oncommit != ONCOMMIT_NOOP &&
+		stmt->relation->relpersistence != RELPERSISTENCE_TEMP &&
+		stmt->relation->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 				 errmsg("ON COMMIT can only be used on temporary tables")));
+	if (stmt->oncommit == ONCOMMIT_DROP &&
+		stmt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errmsg("ON COMMIT DROP cannot be used on global temporary tables")));
 
 	if (stmt->partspec != NULL)
 	{
@@ -1704,7 +1712,8 @@ RemoveRelations(DropStmt *drop)
 		 * callback retrieved the rel's persistence for us.
 		 */
 		if (drop->concurrent &&
-			state.actual_relpersistence != RELPERSISTENCE_TEMP)
+			state.actual_relpersistence != RELPERSISTENCE_TEMP &&
+			state.actual_relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
 		{
 			Assert(list_length(drop->objects) == 1 &&
 				   drop->removeType == OBJECT_INDEX);
@@ -2758,14 +2767,17 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 
 		/*
 		 * If the parent is permanent, so must be all of its partitions.  Note
-		 * that inheritance allows that case.
+		 * that inheritance allows that case.  For these purposes, global
+		 * temporary tables behave like permanent tables.
 		 */
 		if (is_partition &&
 			relation->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
 			relpersistence == RELPERSISTENCE_TEMP)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+					 errmsg(RELATION_IS_GLOBAL_TEMP(relation)
+							? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
+							: "cannot create a temporary relation as partition of permanent relation \"%s\"",
 							RelationGetRelationName(relation))));
 
 		/* Permanent rels cannot inherit from temporary ones */
@@ -2775,6 +2787,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg(!is_partition
 							? "cannot inherit from temporary relation \"%s\""
+							: relpersistence == RELPERSISTENCE_GLOBAL_TEMP
+							? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
 							: "cannot create a permanent relation as partition of temporary relation \"%s\"",
 							RelationGetRelationName(relation))));
 
@@ -3834,10 +3848,12 @@ SetRelationTableSpace(Relation rel,
 	UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
 
 	/*
-	 * Record dependency on tablespace.  This is only required for relations
-	 * that have no physical storage.
+	 * Record dependency on tablespace.  This is required for relations that
+	 * have no physical storage, and for global temporary relations whose
+	 * physical storage is temporary.
 	 */
-	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
+		RELATION_IS_GLOBAL_TEMP(rel))
 		changeDependencyOnTablespace(RelationRelationId, reloid,
 									 rd_rel->reltablespace);
 
@@ -6005,6 +6021,18 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("cannot rewrite temporary tables of other sessions")));
 
+			/*
+			 * Don't allow rewrite on global temporary tables, if they're
+			 * being used by other backends ... we have no way to rewrite
+			 * local storage of another backend.
+			 */
+			if (RELATION_IS_GLOBAL_TEMP(OldHeap) &&
+				IsOtherUsingGlobalTempRelation(RelationGetRelid(OldHeap)))
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("cannot rewrite global temporary table \"%s\" because it is being used in another session",
+							   RelationGetRelationName(OldHeap)));
+
 			/*
 			 * Select destination tablespace (same as original unless user
 			 * requested a change)
@@ -6101,11 +6129,22 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			/*
 			 * If required, test the current data within the table against new
 			 * constraints generated by ALTER TABLE commands, but don't
-			 * rebuild data.
+			 * rebuild data.  Don't allow this for global temporary tables, if
+			 * they're being used by other backends ... we have no way to scan
+			 * local storage of another backend.
 			 */
 			if (tab->constraints != NIL || tab->verify_new_notnull ||
 				tab->partition_constraint != NULL)
+			{
+				if (tab->oldrelpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+					IsOtherUsingGlobalTempRelation(tab->relid))
+					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",
+								   get_rel_name(tab->relid)));
+
 				ATRewriteTable(tab, InvalidOid);
+			}
 
 			/*
 			 * If we had SET TABLESPACE but no reason to reconstruct tuples,
@@ -6667,6 +6706,7 @@ ATGetQueueEntry(List **wqueue, Relation rel)
 	tab->rel = NULL;			/* set later */
 	tab->relkind = rel->rd_rel->relkind;
 	tab->oldDesc = CreateTupleDescCopyConstr(RelationGetDescr(rel));
+	tab->oldrelpersistence = rel->rd_rel->relpersistence;
 	tab->newAccessMethod = InvalidOid;
 	tab->chgAccessMethod = false;
 	tab->newTableSpace = InvalidOid;
@@ -10229,11 +10269,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-						 errmsg("constraints on temporary tables may reference only temporary tables")));
+						 errmsg("constraints on local temporary tables may reference only local temporary tables")));
 			if (!pkrel->rd_islocaltemp || !rel->rd_islocaltemp)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-						 errmsg("constraints on temporary tables must involve temporary tables of this session")));
+						 errmsg("constraints on local temporary tables must involve local temporary tables of this session")));
+			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			if (!RELATION_IS_GLOBAL_TEMP(pkrel))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("constraints on global temporary tables may reference only global temporary tables")));
 			break;
 	}
 
@@ -17420,7 +17466,8 @@ index_copy_data(Relation rel, RelFileLocator newrlocator)
 	 * NOTE: any conflict in relfilenumber value will be caught in
 	 * RelationCreateStorage().
 	 */
-	dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true);
+	dstrel = RelationCreateStorage(rel->rd_id, newrlocator,
+								   rel->rd_rel->relpersistence, true);
 
 	/* copy main fork */
 	RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM,
@@ -19081,6 +19128,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	switch (rel->rd_rel->relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 errmsg("cannot change logged status of table \"%s\" because it is temporary",
@@ -20689,7 +20737,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 		attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
+				 errmsg(RELATION_IS_GLOBAL_TEMP(rel)
+						? "cannot attach a local temporary relation as partition of global temporary relation \"%s\""
+						: "cannot attach a temporary relation as partition of permanent relation \"%s\"",
 						RelationGetRelationName(rel))));
 
 	/* Temp parent cannot have a partition that is itself not a temp */
@@ -20697,7 +20747,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 		attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
+				 errmsg(RELATION_IS_GLOBAL_TEMP(attachrel)
+						? "cannot attach a global temporary relation as partition of local temporary relation \"%s\""
+						: "cannot attach a permanent relation as partition of temporary relation \"%s\"",
 						RelationGetRelationName(rel))));
 
 	/* If the parent is temp, it must belong to this session */
@@ -22775,7 +22827,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
 		newPartName->relpersistence == RELPERSISTENCE_TEMP)
 		ereport(ERROR,
 				errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+				errmsg(parent_relform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
+					   ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
+					   : "cannot create a temporary relation as partition of permanent relation \"%s\"",
 					   RelationGetRelationName(parent_rel)));
 
 	/* Permanent rels cannot be partitions belonging to a temporary parent. */
@@ -22783,7 +22837,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
 		parent_relform->relpersistence == RELPERSISTENCE_TEMP)
 		ereport(ERROR,
 				errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
+				errmsg(newPartName->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
+					   ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
+					   : "cannot create a permanent relation as partition of temporary relation \"%s\"",
 					   RelationGetRelationName(parent_rel)));
 
 	/* Create the relation. */
diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index d91fcf0facf..aa3093fb2f0 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -1159,7 +1159,8 @@ GetDefaultTablespace(char relpersistence, bool partitioned)
 	Oid			result;
 
 	/* The temp-table case is handled elsewhere */
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP ||
+		relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		PrepareTempTablespaces();
 		return GetNextTempTableSpace();
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 1bd78a4cdf0..618963880c0 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -476,6 +476,15 @@ DefineView(ViewStmt *stmt, const char *queryString,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("views cannot be unlogged because they do not have storage")));
 
+	/*
+	 * Global temporary views are not sensible either.  This used to generate
+	 * a warning in the parser, but now we raise an error.
+	 */
+	if (stmt->view->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("views cannot be global temporary because they do not have storage")));
+
 	/*
 	 * If the user didn't explicitly ask for a temporary view, check whether
 	 * we need one implicitly.  We allow TEMP to be inserted automatically as
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index c134594a21a..efbc2b754d3 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -645,6 +645,8 @@ static void
 set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 						  RangeTblEntry *rte)
 {
+	char		relpersistence;
+
 	/*
 	 * The flag has previously been initialized to false, so we can just
 	 * return if it becomes clear that we can't safely set it.
@@ -672,7 +674,9 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 			 * the rest of the necessary infrastructure right now anyway.  So
 			 * for now, bail out if we see a temporary table.
 			 */
-			if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
+			relpersistence = get_rel_persistence(rte->relid);
+			if (relpersistence == RELPERSISTENCE_TEMP ||
+				relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 				return;
 
 			/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..ac659e43fe4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3913,31 +3913,21 @@ CreateStmt:	CREATE OptTemp TABLE qualified_name '(' OptTableElementList ')'
  * Redundancy here is needed to avoid shift/reduce conflicts,
  * since TEMP is not a reserved word.  See also OptTempTableName.
  *
- * NOTE: we accept both GLOBAL and LOCAL options.  They currently do nothing,
- * but future versions might consider GLOBAL to request SQL-spec-compliant
- * temp table behavior, so warn about that.  Since we have no modules the
+ * NOTE: we accept both GLOBAL and LOCAL options.  GLOBAL results in
+ * SQL-spec-compliant temp table behavior.  Since we have no modules, the
  * LOCAL keyword is really meaningless; furthermore, some other products
- * implement LOCAL as meaning the same as our default temp table behavior,
- * so we'll probably continue to treat LOCAL as a noise word.
+ * implement LOCAL as meaning the same as our default temp table behavior of
+ * keeping the table definition private to the session that created it, and
+ * dropping the table at the end of the session, so we just treat LOCAL as a
+ * noise word.  (Actually, the SQL-spec mandates that either GLOBAL or LOCAL
+ * must be specified, but we allow it to be omitted.)
  */
 OptTemp:	TEMPORARY					{ $$ = RELPERSISTENCE_TEMP; }
 			| TEMP						{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMPORARY			{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMP				{ $$ = RELPERSISTENCE_TEMP; }
-			| GLOBAL TEMPORARY
-				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
-					$$ = RELPERSISTENCE_TEMP;
-				}
-			| GLOBAL TEMP
-				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
-					$$ = RELPERSISTENCE_TEMP;
-				}
+			| GLOBAL TEMPORARY			{ $$ = RELPERSISTENCE_GLOBAL_TEMP; }
+			| GLOBAL TEMP				{ $$ = RELPERSISTENCE_GLOBAL_TEMP; }
 			| UNLOGGED					{ $$ = RELPERSISTENCE_UNLOGGED; }
 			| /*EMPTY*/					{ $$ = RELPERSISTENCE_PERMANENT; }
 		;
@@ -13972,19 +13962,13 @@ OptTempTableName:
 				}
 			| GLOBAL TEMPORARY opt_table qualified_name
 				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
 					$$ = $4;
-					$$->relpersistence = RELPERSISTENCE_TEMP;
+					$$->relpersistence = RELPERSISTENCE_GLOBAL_TEMP;
 				}
 			| GLOBAL TEMP opt_table qualified_name
 				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
 					$$ = $4;
-					$$->relpersistence = RELPERSISTENCE_TEMP;
+					$$->relpersistence = RELPERSISTENCE_GLOBAL_TEMP;
 				}
 			| UNLOGGED opt_table qualified_name
 				{
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index e9aaf24c1be..3f6ddcc5aa9 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2050,7 +2050,10 @@ do_autovacuum(void)
 
 		/*
 		 * Check if it is a temp table (presumably, of some other backend's).
-		 * We cannot safely process other backends' temp tables.
+		 * We cannot safely process other backends' local temp tables, so just
+		 * record any that appear to be orphaned, so we can drop them later.
+		 * Global temporary tables cannot be processed either, but they cannot
+		 * be orphaned in this way either, so we simply ignore them.
 		 */
 		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
 		{
@@ -2072,6 +2075,8 @@ do_autovacuum(void)
 			}
 			continue;
 		}
+		else if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			continue;
 
 		/* Fetch reloptions and the pgstat entry for this table */
 		relopts = extract_autovac_opts(tuple, pg_class_desc);
@@ -2149,7 +2154,8 @@ do_autovacuum(void)
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 			continue;
 
 		relid = classForm->oid;
@@ -2244,7 +2250,8 @@ do_autovacuum(void)
 		 */
 		if (!((classForm->relkind == RELKIND_RELATION ||
 			   classForm->relkind == RELKIND_MATVIEW) &&
-			  classForm->relpersistence == RELPERSISTENCE_TEMP))
+			  (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			   classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)))
 		{
 			UnlockRelationOid(relid, AccessExclusiveLock);
 			continue;
@@ -3679,7 +3686,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
 			form->relkind != RELKIND_MATVIEW &&
 			form->relkind != RELKIND_TOASTVALUE)
 			continue;
-		if (form->relpersistence == RELPERSISTENCE_TEMP)
+		if (form->relpersistence == RELPERSISTENCE_TEMP ||
+			form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 			continue;
 
 		avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 04f1a268845..d07c85559e4 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -192,6 +192,7 @@
 #include "access/xact.h"
 #include "access/xlog.h"
 #include "access/xloginsert.h"
+#include "catalog/global_temp.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_database.h"
@@ -1458,6 +1459,11 @@ BuildRelationList(bool temp_relations, bool include_shared)
 			if (!temp_relations)
 				continue;
 		}
+		else if (pgc->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			/* Deal with global temporary relations separately below */
+			continue;
+		}
 		else
 		{
 			/*
@@ -1484,6 +1490,23 @@ BuildRelationList(bool temp_relations, bool include_shared)
 
 	CommitTransactionCommand();
 
+	/*
+	 * If we were asked for temporary relations, include all global temporary
+	 * relations currently in use.  This list can be out of date as soon as it
+	 * is returned, but that doesn't matter because we only need to worry
+	 * about those that were in use when the "inprogress-on" state was set,
+	 * and are still in use now.  This does not require database access.
+	 */
+	if (temp_relations)
+	{
+		List	   *gtrs_in_use;
+
+		gtrs_in_use = AllGlobalTempRelationsInUse(MyDatabaseId);
+
+		RelationList = list_concat(RelationList, gtrs_in_use);
+		list_free(gtrs_in_use);
+	}
+
 	return RelationList;
 }
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d6c0cc1f6d4..4f66657ff29 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1236,6 +1236,7 @@ PinBufferForBlock(Relation rel,
 
 	/* Persistence should be set before */
 	Assert((persistence == RELPERSISTENCE_TEMP ||
+			persistence == RELPERSISTENCE_GLOBAL_TEMP ||
 			persistence == RELPERSISTENCE_PERMANENT ||
 			persistence == RELPERSISTENCE_UNLOGGED));
 
@@ -1245,7 +1246,8 @@ PinBufferForBlock(Relation rel,
 									   smgr->smgr_rlocator.locator.relNumber,
 									   smgr->smgr_rlocator.backend);
 
-	if (persistence == RELPERSISTENCE_TEMP)
+	if (persistence == RELPERSISTENCE_TEMP ||
+		persistence == RELPERSISTENCE_GLOBAL_TEMP)
 		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
 	else
 		bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
@@ -1327,7 +1329,8 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		IOContext	io_context;
 		IOObject	io_object;
 
-		if (persistence == RELPERSISTENCE_TEMP)
+		if (persistence == RELPERSISTENCE_TEMP ||
+			persistence == RELPERSISTENCE_GLOBAL_TEMP)
 		{
 			io_context = IOCONTEXT_NORMAL;
 			io_object = IOOBJECT_TEMP_RELATION;
@@ -1391,7 +1394,8 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot access temporary tables of other sessions")));
 
-	if (operation->persistence == RELPERSISTENCE_TEMP)
+	if (operation->persistence == RELPERSISTENCE_TEMP ||
+		operation->persistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_TEMP_RELATION;
@@ -1692,7 +1696,8 @@ TrackBufferHit(IOObject io_object, IOContext io_context,
 									  smgr->smgr_rlocator.backend,
 									  true);
 
-	if (persistence == RELPERSISTENCE_TEMP)
+	if (persistence == RELPERSISTENCE_TEMP ||
+		persistence == RELPERSISTENCE_GLOBAL_TEMP)
 		pgBufferUsage.local_blks_hit += 1;
 	else
 		pgBufferUsage.shared_blks_hit += 1;
@@ -1763,7 +1768,8 @@ WaitReadBuffers(ReadBuffersOperation *operation)
 	IOObject	io_object;
 	bool		needed_wait = false;
 
-	if (operation->persistence == RELPERSISTENCE_TEMP)
+	if (operation->persistence == RELPERSISTENCE_TEMP ||
+		operation->persistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_TEMP_RELATION;
@@ -1953,7 +1959,8 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	instr_time	io_start;
 	StartBufferIOResult status;
 
-	if (persistence == RELPERSISTENCE_TEMP)
+	if (persistence == RELPERSISTENCE_TEMP ||
+		persistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_TEMP_RELATION;
@@ -1972,7 +1979,8 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	if (flags & READ_BUFFERS_SYNCHRONOUSLY)
 		ioh_flags |= PGAIO_HF_SYNCHRONOUS;
 
-	if (persistence == RELPERSISTENCE_TEMP)
+	if (persistence == RELPERSISTENCE_TEMP ||
+		persistence == RELPERSISTENCE_GLOBAL_TEMP)
 		ioh_flags |= PGAIO_HF_REFERENCES_LOCAL;
 
 	/*
@@ -2133,7 +2141,8 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	pgaio_io_set_handle_data_32(ioh, (uint32 *) io_buffers, io_buffers_len);
 
 	pgaio_io_register_callbacks(ioh,
-								persistence == RELPERSISTENCE_TEMP ?
+								persistence == RELPERSISTENCE_TEMP ||
+								persistence == RELPERSISTENCE_GLOBAL_TEMP ?
 								PGAIO_HCB_LOCAL_BUFFER_READV :
 								PGAIO_HCB_SHARED_BUFFER_READV,
 								flags);
@@ -2156,7 +2165,8 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	pgstat_count_io_op_time(io_object, io_context, IOOP_READ,
 							io_start, 1, io_buffers_len * BLCKSZ);
 
-	if (persistence == RELPERSISTENCE_TEMP)
+	if (persistence == RELPERSISTENCE_TEMP ||
+		persistence == RELPERSISTENCE_GLOBAL_TEMP)
 		pgBufferUsage.local_blks_read += io_buffers_len;
 	else
 		pgBufferUsage.shared_blks_read += io_buffers_len;
@@ -2766,7 +2776,8 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										 BMR_GET_SMGR(bmr)->smgr_rlocator.backend,
 										 extend_by);
 
-	if (bmr.relpersistence == RELPERSISTENCE_TEMP)
+	if (bmr.relpersistence == RELPERSISTENCE_TEMP ||
+		bmr.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		first_block = ExtendBufferedRelLocal(bmr, fork, flags,
 											 extend_by, extend_upto,
 											 buffers, &extend_by);
@@ -5486,9 +5497,10 @@ CreateAndCopyRelationData(RelFileLocator src_rlocator,
 	 * Create and copy all forks of the relation.  During create database we
 	 * have a separate cleanup mechanism which deletes complete database
 	 * directory.  Therefore, each individual relation doesn't need to be
-	 * registered for cleanup.
+	 * registered for cleanup.  Also, the relid isn't needed, since it's not a
+	 * global temporary relation.
 	 */
-	RelationCreateStorage(dst_rlocator, relpersistence, false);
+	RelationCreateStorage(InvalidOid, dst_rlocator, relpersistence, false);
 
 	/* copy main fork. */
 	RelationCopyStorageUsingBuffer(src_rlocator, dst_rlocator, MAIN_FORKNUM,
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 560659f9568..f99703bd790 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ WaitLSN	"Waiting to read or update shared Wait-for-LSN state."
 LogicalDecodingControl	"Waiting to read or update logical decoding status information."
 DataChecksumsWorker	"Waiting for data checksums worker."
 AioWorkerControl	"Waiting to update AIO worker information."
+GlobalTempRelControl	"Waiting to update global temporary relation information."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
@@ -417,6 +418,8 @@ XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
 ShmemIndex	"Waiting to find or allocate space in shared memory."
+GlobalTempRelDSA	"Waiting for global temporary relation dynamic shared memory allocation."
+GlobalTempRelHash	"Waiting to access global temporary relation shared usage table."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..e09ea8fe220 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1032,6 +1032,9 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 				Assert(backend != INVALID_PROC_NUMBER);
 			}
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			backend = ProcNumberForTempRelations();
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c", relform->relpersistence);
 			backend = INVALID_PROC_NUMBER;	/* placate compiler */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0572ab424e7..62bbc710358 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+#include "catalog/global_temp.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/partition.h"
@@ -1170,8 +1171,9 @@ retry:
 			else
 			{
 				/*
-				 * If it's a temp table, but not one of ours, we have to use
-				 * the slow, grotty method to figure out the owning backend.
+				 * If it's a local temp table, but not one of ours, we have to
+				 * use the slow, grotty method to figure out the owning
+				 * backend.
 				 *
 				 * Note: it's possible that rd_backend gets set to
 				 * MyProcNumber here, in case we are looking at a pg_class
@@ -1188,6 +1190,10 @@ retry:
 				relation->rd_islocaltemp = false;
 			}
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			relation->rd_backend = ProcNumberForTempRelations();
+			relation->rd_islocaltemp = false;
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c",
 				 relation->rd_rel->relpersistence);
@@ -2112,6 +2118,14 @@ RelationIdGetRelation(Oid relationId)
 		{
 			RelationRebuildRelation(rd);
 
+			/*
+			 * If it's a global temporary relation, make sure it has been
+			 * initialized for use in this backend (a prior initialization
+			 * might have been rolled back).
+			 */
+			if (RELATION_IS_GLOBAL_TEMP(rd))
+				InitGlobalTempRelation(rd);
+
 			/*
 			 * Normally entries need to be valid here, but before the relcache
 			 * has been initialized, not enough infrastructure exists to
@@ -2131,7 +2145,11 @@ RelationIdGetRelation(Oid relationId)
 	 */
 	rd = RelationBuildDesc(relationId, true);
 	if (RelationIsValid(rd))
+	{
 		RelationIncrementReferenceCount(rd);
+		if (RELATION_IS_GLOBAL_TEMP(rd))
+			InitGlobalTempRelation(rd);
+	}
 	return rd;
 }
 
@@ -2205,6 +2223,21 @@ RelationDecrementReferenceCount(Relation rel)
 		ResourceOwnerForgetRelationRef(CurrentResourceOwner, rel);
 }
 
+/*
+ * RelationMarkInvalid
+ *		Mark a relation as invalid, if it's in the relcache, forcing it to be
+ *		reloaded on next access.
+ */
+void
+RelationMarkInvalid(Oid relid)
+{
+	Relation	relation;
+
+	RelationIdCacheLookup(relid, relation);
+	if (RelationIsValid(relation) && relation->rd_isvalid)
+		RelationInvalidateRelation(relation);
+}
+
 /*
  * RelationClose - close an open relation
  *
@@ -2954,6 +2987,9 @@ RelationCacheInvalidateEntry(Oid relationId)
 			if (in_progress_list[i].reloid == relationId)
 				in_progress_list[i].invalidated = true;
 	}
+
+	/* Additional processing required for global temporary relations */
+	InvalidateGlobalTempRelation(relationId);
 }
 
 /*
@@ -3098,6 +3134,9 @@ RelationCacheInvalidate(bool debug_discard)
 		/* Any RelationBuildDesc() on the stack must start over. */
 		for (i = 0; i < in_progress_list_len; i++)
 			in_progress_list[i].invalidated = true;
+
+	/* Invalidate all in-use global temporary relations */
+	InvalidateGlobalTempRelation(InvalidOid);
 }
 
 static void
@@ -3653,6 +3692,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
+			Assert(!isTempOrTempToastNamespace(relnamespace));
 			rel->rd_backend = INVALID_PROC_NUMBER;
 			rel->rd_islocaltemp = false;
 			break;
@@ -3661,6 +3701,11 @@ RelationBuildLocalRelation(const char *relname,
 			rel->rd_backend = ProcNumberForTempRelations();
 			rel->rd_islocaltemp = true;
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			Assert(!isTempOrTempToastNamespace(relnamespace));
+			rel->rd_backend = ProcNumberForTempRelations();
+			rel->rd_islocaltemp = false;
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c", relpersistence);
 			break;
@@ -3882,7 +3927,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 		/* handle these directly, at least for now */
 		SMgrRelation srel;
 
-		srel = RelationCreateStorage(newrlocator, persistence, true);
+		srel = RelationCreateStorage(relation->rd_id, newrlocator,
+									 persistence, true);
 		smgrclose(srel);
 	}
 	else
diff --git a/src/backend/utils/cache/relfilenumbermap.c b/src/backend/utils/cache/relfilenumbermap.c
index 6f970fafa05..1f9fe87ebb0 100644
--- a/src/backend/utils/cache/relfilenumbermap.c
+++ b/src/backend/utils/cache/relfilenumbermap.c
@@ -213,7 +213,8 @@ RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber)
 		{
 			Form_pg_class classform = (Form_pg_class) GETSTRUCT(ntp);
 
-			if (classform->relpersistence == RELPERSISTENCE_TEMP)
+			if (classform->relpersistence == RELPERSISTENCE_TEMP ||
+				classform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 				continue;
 
 			if (found)
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 09ba0596400..27497d8bca3 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -859,7 +859,8 @@ prepare_heap_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 
 	appendPQExpBuffer(sql,
 					  "\n) v WHERE c.oid = %u "
-					  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP),
+					  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+					  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP),
 					  rel->reloid);
 }
 
@@ -893,6 +894,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
 						  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+						  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " "
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
@@ -908,6 +910,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
 						  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+						  "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " "
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
@@ -1954,8 +1957,9 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
 	 * until firing off the amcheck command, as the state of an index may
 	 * change by then.
 	 */
-	appendPQExpBufferStr(&sql, "\nWHERE c.relpersistence != "
-						 CppAsString2(RELPERSISTENCE_TEMP));
+	appendPQExpBufferStr(&sql,
+						 "\nWHERE c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP)
+						 "\nAND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
 	if (opts.excludetbl || opts.excludeidx || opts.excludensp)
 		appendPQExpBufferStr(&sql, "\nAND ep.pattern_id IS NULL");
 
@@ -2024,7 +2028,8 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
 								 "\nAND (t.relname ~ ep.rel_regex OR ep.rel_regex IS NULL)"
 								 "\nAND ep.heap_only"
 								 "\nWHERE ep.pattern_id IS NULL"
-								 "\nAND t.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP));
+								 "\nAND t.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP)
+								 "\nAND t.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
 		appendPQExpBufferStr(&sql,
 							 "\n)");
 	}
@@ -2043,7 +2048,8 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
 							 "ON r.oid = i.indrelid "
 							 "INNER JOIN pg_catalog.pg_class c "
 							 "ON i.indexrelid = c.oid "
-							 "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP));
+							 "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+							 "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
 		if (opts.excludeidx || opts.excludensp)
 			appendPQExpBufferStr(&sql,
 								 "\nINNER JOIN pg_catalog.pg_namespace n "
@@ -2082,7 +2088,8 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
 							 "ON t.oid = i.indrelid"
 							 "\nINNER JOIN pg_catalog.pg_class c "
 							 "ON i.indexrelid = c.oid "
-							 "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP));
+							 "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+							 "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
 		if (opts.excludeidx)
 			appendPQExpBufferStr(&sql,
 								 "\nLEFT OUTER JOIN exclude_pat ep "
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c56437d6057..c31e15aff7a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3050,6 +3050,10 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
 	if (tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
 		return;
 
+	/* Don't dump data in global temporary tables */
+	if (tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		return;
+
 	/* Don't dump data in unlogged tables, if so requested */
 	if (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED &&
 		dopt->no_unlogged_table_data)
@@ -17455,7 +17459,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		appendPQExpBuffer(q, "CREATE %s%s %s",
 						  (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED &&
 						   tbinfo->relkind != RELKIND_PARTITIONED_TABLE) ?
-						  "UNLOGGED " : "",
+						  "UNLOGGED " :
+						  tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+						  "GLOBAL TEMP " : "",
 						  reltypename,
 						  qualrelname);
 
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 37fff93892f..91924857158 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -503,6 +503,9 @@ get_rel_infos_query(void)
 					  "         ON c.relnamespace = n.oid "
 					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
 					  CppAsString2(RELKIND_MATVIEW) "%s) AND "
+	/* exclude global temporary tables */
+					  "    relpersistence != "
+					  CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " AND "
 	/* exclude possible orphaned temp tables */
 					  "    ((n.nspname !~ '^pg_temp_' AND "
 					  "      n.nspname !~ '^pg_toast_temp_' AND "
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index af3935b0078..34daeba6e36 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2138,13 +2138,23 @@ describeOneTableDetails(const char *schemaname,
 			if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
 				printfPQExpBuffer(&title, _("Unlogged table \"%s.%s\""),
 								  schemaname, relationname);
+			else if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+				printfPQExpBuffer(&title, _("Temporary table \"%s.%s\""),
+								  schemaname, relationname);
+			else if (tableinfo.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+				printfPQExpBuffer(&title, _("Global temporary table \"%s.%s\""),
+								  schemaname, relationname);
 			else
 				printfPQExpBuffer(&title, _("Table \"%s.%s\""),
 								  schemaname, relationname);
 			break;
 		case RELKIND_VIEW:
-			printfPQExpBuffer(&title, _("View \"%s.%s\""),
-							  schemaname, relationname);
+			if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+				printfPQExpBuffer(&title, _("Temporary view \"%s.%s\""),
+								  schemaname, relationname);
+			else
+				printfPQExpBuffer(&title, _("View \"%s.%s\""),
+								  schemaname, relationname);
 			break;
 		case RELKIND_MATVIEW:
 			printfPQExpBuffer(&title, _("Materialized view \"%s.%s\""),
@@ -2182,6 +2192,12 @@ describeOneTableDetails(const char *schemaname,
 			if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
 				printfPQExpBuffer(&title, _("Unlogged partitioned table \"%s.%s\""),
 								  schemaname, relationname);
+			else if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+				printfPQExpBuffer(&title, _("Temporary partitioned table \"%s.%s\""),
+								  schemaname, relationname);
+			else if (tableinfo.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+				printfPQExpBuffer(&title, _("Global temporary partitioned table \"%s.%s\""),
+								  schemaname, relationname);
 			else
 				printfPQExpBuffer(&title, _("Partitioned table \"%s.%s\""),
 								  schemaname, relationname);
@@ -4310,10 +4326,12 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 						  ",\n  CASE c.relpersistence "
 						  "WHEN " CppAsString2(RELPERSISTENCE_PERMANENT) " THEN '%s' "
 						  "WHEN " CppAsString2(RELPERSISTENCE_TEMP) " THEN '%s' "
+						  "WHEN " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " THEN '%s' "
 						  "WHEN " CppAsString2(RELPERSISTENCE_UNLOGGED) " THEN '%s' "
 						  "END as \"%s\"",
 						  gettext_noop("permanent"),
 						  gettext_noop("temporary"),
+						  gettext_noop("global temporary"),
 						  gettext_noop("unlogged"),
 						  gettext_noop("Persistence"));
 		translate_columns[cols_so_far] = true;
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 67a7665c5d7..b9bbf204cde 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -624,7 +624,9 @@ retrieve_objects(PGconn *conn, vacuumingOptions *vacopts,
 	 */
 	appendPQExpBufferStr(&catalog_query,
 						 " WHERE c.relpersistence OPERATOR(pg_catalog.!=) "
-						 CppAsString2(RELPERSISTENCE_TEMP) "\n");
+						 CppAsString2(RELPERSISTENCE_TEMP)
+						 "\nAND c.relpersistence OPERATOR(pg_catalog.!=) "
+						 CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) "\n");
 
 	/*
 	 * Used to match the tables or schemas listed by the user, for the WHERE
diff --git a/src/include/catalog/global_temp.h b/src/include/catalog/global_temp.h
new file mode 100644
index 00000000000..c27caab12dc
--- /dev/null
+++ b/src/include/catalog/global_temp.h
@@ -0,0 +1,42 @@
+/*-------------------------------------------------------------------------
+ *
+ * global_temp.h
+ *	  Global temporary relation management.
+ *
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/global_temp.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef GLOBAL_TEMP_H
+#define GLOBAL_TEMP_H
+
+extern void TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
+										   ProcNumber backend, bool create);
+
+extern void ReassignGlobalTempRelationStorage(RelFileLocator rlocator,
+											  Oid newRelid);
+
+extern void InitGlobalTempRelation(Relation relation);
+
+extern void InvalidateGlobalTempRelation(Oid relid);
+
+extern void GlobalTempRelationCreated(Relation relation);
+
+extern void GlobalTempRelationDropped(Oid relid);
+
+extern void PreCommit_GlobalTempRelation(void);
+
+extern void AtEOXact_GlobalTempRelation(bool isCommit);
+
+extern void AtEOSubXact_GlobalTempRelation(bool isCommit,
+										   SubTransactionId mySubid,
+										   SubTransactionId parentSubid);
+
+extern bool IsOtherUsingGlobalTempRelation(Oid relid);
+
+extern List *AllGlobalTempRelationsInUse(Oid dbId);
+
+#endif							/* GLOBAL_TEMP_H */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index c4af599dc90..0ad4ead53e4 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -182,7 +182,8 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);
 
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
-#define		  RELPERSISTENCE_TEMP		't' /* temporary table */
+#define		  RELPERSISTENCE_TEMP		't' /* temp table (in temp schema) */
+#define		  RELPERSISTENCE_GLOBAL_TEMP 'g'	/* global temporary table */
 
 /* default selection for replica identity (primary key or nothing) */
 #define		  REPLICA_IDENTITY_DEFAULT	'd'
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 70f619a6d6f..7827c4ba710 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -22,7 +22,7 @@
 /* GUC variables */
 extern PGDLLIMPORT int wal_skip_threshold;
 
-extern SMgrRelation RelationCreateStorage(RelFileLocator rlocator,
+extern SMgrRelation RelationCreateStorage(Oid relid, RelFileLocator rlocator,
 										  char relpersistence,
 										  bool register_delete);
 extern void RelationDropStorage(Relation rel);
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index d7eb648bd27..557ff4dcbe3 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN)
 PG_LWLOCK(55, LogicalDecodingControl)
 PG_LWLOCK(56, DataChecksumsWorker)
 PG_LWLOCK(57, AioWorkerControl)
+PG_LWLOCK(58, GlobalTempRelControl)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
@@ -140,3 +141,5 @@ PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
 PG_LWLOCKTRANCHE(SHMEM_INDEX, ShmemIndex)
+PG_LWLOCKTRANCHE(GLOBAL_TEMP_REL_DSA, GlobalTempRelDSA)
+PG_LWLOCKTRANCHE(GLOBAL_TEMP_REL_HASH, GlobalTempRelHash)
diff --git a/src/include/storage/subsystemlist.h b/src/include/storage/subsystemlist.h
index 9ad619080be..89378d1d87f 100644
--- a/src/include/storage/subsystemlist.h
+++ b/src/include/storage/subsystemlist.h
@@ -88,3 +88,6 @@ PG_SHMEM_SUBSYSTEM(DataChecksumsShmemCallbacks)
 
 /* AIO subsystem. This delegates to the method-specific callbacks */
 PG_SHMEM_SUBSYSTEM(AioShmemCallbacks)
+
+/* global temporary relation usage table */
+PG_SHMEM_SUBSYSTEM(GlobalTempRelShmemCallbacks)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fa07ebf8ff7..7737766b723 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -58,7 +58,7 @@ typedef struct RelationData
 	SMgrRelation rd_smgr;		/* cached file handle, or NULL */
 	int			rd_refcnt;		/* reference count */
 	ProcNumber	rd_backend;		/* owning backend's proc number, if temp rel */
-	bool		rd_islocaltemp; /* rel is a temp rel of this session */
+	bool		rd_islocaltemp; /* rel is a local temp rel of this session */
 	bool		rd_isnailed;	/* rel is nailed in cache */
 	bool		rd_isvalid;		/* relcache entry is valid */
 	bool		rd_indexvalid;	/* is rd_indexlist valid? (also rd_pkindex and
@@ -646,13 +646,15 @@ RelationCloseSmgr(Relation relation)
  *		True if relation's pages are stored in local buffers.
  */
 #define RelationUsesLocalBuffers(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP || \
+	 (relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 
 /*
  * RELATION_IS_LOCAL
- *		If a rel is either temp or newly created in the current transaction,
- *		it can be assumed to be accessible only to the current backend.
- *		This is typically used to decide that we can skip acquiring locks.
+ *		If a rel is either local temp or newly created in the current
+ *		transaction, it can be assumed to be accessible only to the current
+ *		backend.  This is typically used to decide that we can skip acquiring
+ *		locks.
  *
  * Beware of multiple eval of argument
  */
@@ -662,7 +664,8 @@ RelationCloseSmgr(Relation relation)
 
 /*
  * RELATION_IS_OTHER_TEMP
- *		Test for a temporary relation that belongs to some other session.
+ *		Test for a local temporary relation that belongs to some other
+ *		session.
  *
  * Reading another session's temp-table data through never works right:
  * the owning session keeps the data in its private local buffer pool,
@@ -679,6 +682,13 @@ RelationCloseSmgr(Relation relation)
 	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP && \
 	 !(relation)->rd_islocaltemp)
 
+/*
+ * RELATION_IS_GLOBAL_TEMP
+ *		True if the relation is a global temporary relation.
+ */
+#define RELATION_IS_GLOBAL_TEMP(relation) \
+	((relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+
 
 /*
  * RelationIsScannable
@@ -727,5 +737,6 @@ RelationCloseSmgr(Relation relation)
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
 extern void RelationDecrementReferenceCount(Relation rel);
+extern void RelationMarkInvalid(Oid relid);
 
 #endif							/* REL_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
new file mode 100644
index 00000000000..7951c3c4910
--- /dev/null
+++ b/src/test/isolation/expected/global-temp.out
@@ -0,0 +1,242 @@
+Parsed test spec with 2 sessions
+
+starting permutation: ins1 ins2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+
+starting permutation: ins1p1 ins1p2 ins2p1 ins2p2 sel1p sel2p
+step ins1p1: INSERT INTO tmp_parted VALUES (1, 's1 p1');
+step ins1p2: INSERT INTO tmp_parted VALUES (2, 's1 p2');
+step ins2p1: INSERT INTO tmp_parted VALUES (1, 's2 p1');
+step ins2p2: INSERT INTO tmp_parted VALUES (2, 's2 p2');
+step sel1p: SELECT tableoid::regclass, * FROM tmp_parted;
+tableoid|key|val  
+--------+---+-----
+tmp_p1  |  1|s1 p1
+tmp_p2  |  2|s1 p2
+(2 rows)
+
+step sel2p: SELECT tableoid::regclass, * FROM tmp_parted;
+tableoid|key|val  
+--------+---+-----
+tmp_p1  |  1|s2 p1
+tmp_p2  |  2|s2 p2
+(2 rows)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 c2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step c2: COMMIT;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 sp2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step sp2: SAVEPOINT sp;
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 sp2 ins2 sel1 sel2 rsp2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step sp2: SAVEPOINT sp;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step rsp2: ROLLBACK TO SAVEPOINT sp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sp2: SAVEPOINT sp;
+step t2: TRUNCATE tmp;
+step rsp2: ROLLBACK TO SAVEPOINT sp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: create1 ins1_2 alter1a alter1b ins2_2 seltype1 seltype2 drop1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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 ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step seltype1: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+  1|numeric  |s1 
+(1 row)
+
+step seltype2: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+  1|numeric  |s2 
+(1 row)
+
+step drop1: DROP TABLE tmp2;
+
+starting permutation: create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+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;
+ERROR:  cannot rewrite global temporary table "tmp2" because it is being used in another session
+step alter1b: ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL;
+ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step seltype1: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+  1|integer  |s1 
+(1 row)
+
+step seltype2: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+  1|integer  |s2 
+(1 row)
+
+step drop1: DROP TABLE tmp2;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..7d1aacec267 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: global-temp
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
new file mode 100644
index 00000000000..32e6e93df07
--- /dev/null
+++ b/src/test/isolation/specs/global-temp.spec
@@ -0,0 +1,56 @@
+# Test global temporary relations
+
+setup {
+  CREATE GLOBAL TEMP TABLE tmp (key int, val text);
+
+  CREATE GLOBAL TEMP TABLE tmp_parted (key int, 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));
+}
+
+teardown {
+  DROP TABLE tmp, tmp_parted;
+}
+
+session s1
+step ins1 { INSERT INTO tmp VALUES (1, 's1'); }
+step ins1p1 { INSERT INTO tmp_parted VALUES (1, 's1 p1'); }
+step ins1p2 { INSERT INTO tmp_parted VALUES (2, 's1 p2'); }
+step sel1 { SELECT * FROM tmp; }
+step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
+step create1 { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text); }
+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 seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step drop1 { DROP TABLE tmp2; }
+
+session s2
+step b2 { BEGIN; }
+step ins2 { INSERT INTO tmp VALUES (1, 's2'); }
+step ins2p1 { INSERT INTO tmp_parted VALUES (1, 's2 p1'); }
+step ins2p2 { INSERT INTO tmp_parted VALUES (2, 's2 p2'); }
+step sel2 { SELECT * FROM tmp; }
+step sel2p { SELECT tableoid::regclass, * FROM tmp_parted; }
+step t2 { TRUNCATE tmp; }
+step c2 { COMMIT; }
+step r2 { ROLLBACK; }
+step sp2 { SAVEPOINT sp; }
+step rsp2 { ROLLBACK TO SAVEPOINT sp; }
+step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
+step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+
+# Basic effects
+permutation ins1 ins2 sel1 sel2
+permutation ins1p1 ins1p2 ins2p1 ins2p2 sel1p sel2p
+
+# Test rollback of GTT initialization
+permutation ins1 b2 ins2 sel1 sel2 c2 sel1 sel2
+permutation ins1 b2 ins2 sel1 sel2 r2 sel1 sel2
+permutation ins1 b2 ins2 sel1 sel2 sp2 r2 sel1 sel2
+permutation ins1 b2 sp2 ins2 sel1 sel2 rsp2 sel1 sel2 r2 sel1 sel2
+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 ins2_2 seltype1 seltype2 drop1
+permutation create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
diff --git a/src/test/modules/test_checksums/t/010_global_temp.pl b/src/test/modules/test_checksums/t/010_global_temp.pl
new file mode 100644
index 00000000000..4355ebe17bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_global_temp.pl
@@ -0,0 +1,56 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# global temporary tables
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('global_temp_table_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create a global temporary table in an interactive psql process.  Should act
+# as a barrier for checksum enablement to block on.
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe(
+	'CREATE GLOBAL TEMPORARY TABLE gtt AS SELECT * FROM generate_series(1, 10000) x;');
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# In another session, make sure we can see the blocking global temporary table
+# but start processing anyways and check that we are blocked with a proper
+# wait event.
+my $result = $node->safe_psql('postgres',
+	"SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'gtt';");
+is($result, 'g', 'ensure we can see the global temporary table');
+
+# Enable, but stop waiting at inprogress-on since it will sit there until the
+# above temporary table is removed.
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Ensure that checksum enablement continues to block
+sleep(1);
+test_checksum_state($node, 'inprogress-on');
+
+# Make sure background session can still read back its data
+$result = $bsession->query_safe('SELECT count(*) FROM gtt WHERE x % 2 = 0;');
+is($result, '5000', 'ensure global temporary table can still be read');
+
+# Quit background session and check that checksum enablement unblocks
+$bsession->quit;
+wait_for_checksum_state($node, 'on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index b891d68d4a7..31cb4134019 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -3593,6 +3593,7 @@ FROM pg_class,
     pg_filenode_relation(reltablespace, pg_relation_filenode(oid)) AS mapped_oid
 WHERE relkind IN ('r', 'i', 'S', 't', 'm')
   AND relpersistence != 't'
+  AND relpersistence != 'g'
   AND mapped_oid IS DISTINCT FROM oid;
 SELECT m.* FROM filenode_mapping m LEFT JOIN pg_class c ON c.oid = m.oid
 WHERE c.oid IS NOT NULL OR m.mapped_oid IS NOT NULL;
@@ -4099,9 +4100,12 @@ DROP TABLE parent CASCADE;
 -- check any TEMP-ness
 CREATE TEMP TABLE temp_parted (a int) PARTITION BY LIST (a);
 CREATE TABLE perm_part (a int);
+CREATE GLOBAL TEMP TABLE global_temp_part (a int);
 ALTER TABLE temp_parted ATTACH PARTITION perm_part FOR VALUES IN (1);
 ERROR:  cannot attach a permanent relation as partition of temporary relation "temp_parted"
-DROP TABLE temp_parted, perm_part;
+ALTER TABLE temp_parted ATTACH PARTITION global_temp_part FOR VALUES IN (1);
+ERROR:  cannot attach a global temporary relation as partition of local temporary relation "temp_parted"
+DROP TABLE temp_parted, perm_part, global_temp_part;
 -- check that the table being attached is not a typed table
 CREATE TYPE mytype AS (a int);
 CREATE TABLE fail_part OF mytype;
diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out
index 86c0957dcc7..6e2ce417a17 100644
--- a/src/test/regress/expected/create_property_graph.out
+++ b/src/test/regress/expected/create_property_graph.out
@@ -108,6 +108,8 @@ SELECT pg_get_propgraphdef('g5'::regclass);
 -- error cases
 CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 ERROR:  property graphs cannot be unlogged because they do not have storage
+CREATE GLOBAL TEMP PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+ERROR:  property graphs cannot be global temporary because they do not have storage
 CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 ERROR:  relation "xx" does not exist
 CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index a7a24fa3adc..3e567f911bb 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -46,7 +46,7 @@ CREATE TABLE pg_temp.implicitly_temp (a int primary key);		-- OK
 CREATE TEMP TABLE explicitly_temp (a int primary key);			-- also OK
 CREATE TEMP TABLE pg_temp.doubly_temp (a int primary key);		-- also OK
 CREATE TEMP TABLE public.temp_to_perm (a int primary key);		-- not OK
-ERROR:  cannot create temporary relation in non-temporary schema
+ERROR:  cannot create local temporary relation in non-temporary schema
 LINE 1: CREATE TEMP TABLE public.temp_to_perm (a int primary key);
                           ^
 DROP TABLE unlogged1, public.unlogged2;
@@ -1054,13 +1054,21 @@ drop table boolspart;
 -- partitions mixing temporary and permanent relations
 create table perm_parted (a int) partition by list (a);
 create temporary table temp_parted (a int) partition by list (a);
+create global temporary table global_temp_parted (a int) partition by list (a);
 create table perm_part partition of temp_parted default; -- error
 ERROR:  cannot create a permanent relation as partition of temporary relation "temp_parted"
+create table perm_part partition of global_temp_parted default; -- ok
 create temp table temp_part partition of perm_parted default; -- error
 ERROR:  cannot create a temporary relation as partition of permanent relation "perm_parted"
+create temp table temp_part partition of global_temp_parted default; -- error
+ERROR:  cannot create a local temporary relation as partition of global temporary relation "global_temp_parted"
 create temp table temp_part partition of temp_parted default; -- ok
+create global temp table global_temp_part partition of temp_parted default; -- error
+ERROR:  cannot create a global temporary relation as partition of local temporary relation "temp_parted"
+create global temp table global_temp_part partition of perm_parted default; -- ok
 drop table perm_parted cascade;
 drop table temp_parted cascade;
+drop table global_temp_parted cascade;
 -- check that adding partitions to a table while it is being used is prevented
 create table tab_part_create (a int) partition by list (a);
 create or replace function func_part_create() returns trigger
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 63cf4b4371d..175376168f1 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -123,7 +123,7 @@ CREATE VIEW temp_view_test.v2 AS SELECT * FROM base_table;
 CREATE VIEW temp_view_test.v3_temp AS SELECT * FROM temp_table;
 NOTICE:  view "v3_temp" will be a temporary view
 DETAIL:  It depends on temporary table temp_table.
-ERROR:  cannot create temporary relation in non-temporary schema
+ERROR:  cannot create local temporary relation in non-temporary schema
 -- should fail
 CREATE SCHEMA test_view_schema
     CREATE TEMP VIEW testview AS SELECT 1;
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
new file mode 100644
index 00000000000..f34bd83b0cb
--- /dev/null
+++ b/src/test/regress/expected/global_temp.out
@@ -0,0 +1,273 @@
+--
+-- GLOBAL TEMP
+--
+CREATE SCHEMA global_temp_tests;
+GRANT USAGE ON SCHEMA global_temp_tests TO PUBLIC;
+SET search_path = global_temp_tests;
+CREATE ROLE regress_global_temp_user;
+GRANT CREATE ON SCHEMA global_temp_tests TO regress_global_temp_user;
+GRANT CREATE ON DATABASE regression TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+-- Test table creation
+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 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);
+\d tmp1
+  Global temporary table "global_temp_tests.tmp1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+
+\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 | 
+(3 rows)
+
+-- Information schema
+SELECT table_catalog, table_schema, table_name, table_type
+FROM information_schema.tables
+WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
+ORDER BY table_name;
+ table_catalog |   table_schema    | table_name |    table_type    
+---------------+-------------------+------------+------------------
+ regression    | global_temp_tests | tmp1       | GLOBAL TEMPORARY
+ regression    | global_temp_xxx   | tmp2       | GLOBAL TEMPORARY
+ regression    | global_temp_yyy   | tmp3       | GLOBAL TEMPORARY
+(3 rows)
+
+DROP SCHEMA global_temp_xxx CASCADE;
+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);
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp2;
+ a 
+---
+(0 rows)
+
+DROP TABLE tmp2;
+-- ON COMMIT DROP not allowed
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DROP; -- fail
+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 
+---
+(0 rows)
+
+PREPARE TRANSACTION 'twophase'; -- fail
+ERROR:  cannot PREPARE a transaction that has operated on temporary objects
+-- Test partitioned global temp table
+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 TABLE tmp2_p2 PARTITION OF tmp2 FOR VALUES IN (2);
+CREATE TEMP TABLE local_tmp (a int);
+ALTER TABLE tmp2 ATTACH PARTITION local_tmp FOR VALUES IN (3); -- fail
+ERROR:  cannot attach a local temporary relation as partition of global temporary relation "tmp2"
+INSERT INTO tmp2 VALUES (1), (2);
+SELECT * FROM tmp2 ORDER BY a;
+ a 
+---
+ 1
+ 2
+(2 rows)
+
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp2 ORDER BY a;
+ a 
+---
+ 2
+(1 row)
+
+DROP TABLE tmp2;
+-- Test ALTER TABLE with rewrite
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+ALTER TABLE tmp2 ALTER COLUMN a SET DATA TYPE numeric;
+SELECT a, pg_typeof(a) FROM tmp2;
+ a | pg_typeof 
+---+-----------
+ 1 | numeric
+(1 row)
+
+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 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;
+-- Test ALTER TABLE ... SET TABLESPACE
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+  regexp_replace   
+-------------------
+ base/NNN/tNNN_NNN
+(1 row)
+
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+            regexp_replace             
+---------------------------------------
+ pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+(1 row)
+
+DROP TABLE tmp2;
+-- Test dependency on tablespace
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int) TABLESPACE temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE temp_test_tablespace; -- fail
+ERROR:  tablespace "temp_test_tablespace" cannot be dropped because some objects depend on it
+DETAIL:  tablespace for table tmp2
+DROP TABLE tmp2;
+DROP TABLESPACE temp_test_tablespace;
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+ALTER TABLE tmp2 SET TABLESPACE temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE temp_test_tablespace; -- fail
+ERROR:  tablespace "temp_test_tablespace" cannot be dropped because some objects depend on it
+DETAIL:  tablespace for table tmp2
+DROP TABLE tmp2;
+DROP TABLESPACE temp_test_tablespace;
+-- Test TRUNCATE
+INSERT INTO tmp1 VALUES (1);
+BEGIN;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+ROLLBACK;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+RELEASE sp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+ROLLBACK;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+ROLLBACK TO sp1;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+-- Test view creation
+INSERT INTO tmp1 VALUES (1);
+CREATE VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+ a 
+---
+ 1
+(1 row)
+
+DROP VIEW v;
+CREATE TEMP VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+ a 
+---
+ 1
+(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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 1d21d3eb446..d441f514a45 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -505,7 +505,7 @@ ORDER BY 1;
 SELECT c1.oid, c1.relname
 FROM pg_class as c1
 WHERE relkind NOT IN ('r', 'i', 'S', 't', 'v', 'm', 'c', 'f', 'p', 'I') OR
-    relpersistence NOT IN ('p', 'u', 't') OR
+    relpersistence NOT IN ('p', 'u', 't', 'g') OR
     relreplident NOT IN ('d', 'n', 'f', 'i');
  oid | relname 
 -----+---------
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..0e8a7491274 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -101,8 +101,10 @@ test: publication subscription
 # ----------
 # Another group of parallel tests
 # select_views depends on create_view
+# NB: global_temp.sql does reconnects which transiently uses 2 connections,
+# so keep this parallel group to at most 19 tests
 # ----------
-test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table
+test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table global_temp
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..9011959e50d 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2210,6 +2210,7 @@ FROM pg_class,
     pg_filenode_relation(reltablespace, pg_relation_filenode(oid)) AS mapped_oid
 WHERE relkind IN ('r', 'i', 'S', 't', 'm')
   AND relpersistence != 't'
+  AND relpersistence != 'g'
   AND mapped_oid IS DISTINCT FROM oid;
 SELECT m.* FROM filenode_mapping m LEFT JOIN pg_class c ON c.oid = m.oid
 WHERE c.oid IS NOT NULL OR m.mapped_oid IS NOT NULL;
@@ -2474,8 +2475,10 @@ DROP TABLE parent CASCADE;
 -- check any TEMP-ness
 CREATE TEMP TABLE temp_parted (a int) PARTITION BY LIST (a);
 CREATE TABLE perm_part (a int);
+CREATE GLOBAL TEMP TABLE global_temp_part (a int);
 ALTER TABLE temp_parted ATTACH PARTITION perm_part FOR VALUES IN (1);
-DROP TABLE temp_parted, perm_part;
+ALTER TABLE temp_parted ATTACH PARTITION global_temp_part FOR VALUES IN (1);
+DROP TABLE temp_parted, perm_part, global_temp_part;
 
 -- check that the table being attached is not a typed table
 CREATE TYPE mytype AS (a int);
diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql
index 85088ae632c..c3dc7157783 100644
--- a/src/test/regress/sql/create_property_graph.sql
+++ b/src/test/regress/sql/create_property_graph.sql
@@ -92,6 +92,7 @@ SELECT pg_get_propgraphdef('g5'::regclass);
 
 -- error cases
 CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+CREATE GLOBAL TEMP PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
 ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t3 KEY (x));  -- duplicate alias
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 80e424e6bda..bc69608e55e 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -686,11 +686,17 @@ drop table boolspart;
 -- partitions mixing temporary and permanent relations
 create table perm_parted (a int) partition by list (a);
 create temporary table temp_parted (a int) partition by list (a);
+create global temporary table global_temp_parted (a int) partition by list (a);
 create table perm_part partition of temp_parted default; -- error
+create table perm_part partition of global_temp_parted default; -- ok
 create temp table temp_part partition of perm_parted default; -- error
+create temp table temp_part partition of global_temp_parted default; -- error
 create temp table temp_part partition of temp_parted default; -- ok
+create global temp table global_temp_part partition of temp_parted default; -- error
+create global temp table global_temp_part partition of perm_parted default; -- ok
 drop table perm_parted cascade;
 drop table temp_parted cascade;
+drop table global_temp_parted cascade;
 
 -- check that adding partitions to a table while it is being used is prevented
 create table tab_part_create (a int) partition by list (a);
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
new file mode 100644
index 00000000000..ceb6c0ac7b7
--- /dev/null
+++ b/src/test/regress/sql/global_temp.sql
@@ -0,0 +1,151 @@
+--
+-- GLOBAL TEMP
+--
+CREATE SCHEMA global_temp_tests;
+GRANT USAGE ON SCHEMA global_temp_tests TO PUBLIC;
+SET search_path = global_temp_tests;
+CREATE ROLE regress_global_temp_user;
+GRANT CREATE ON SCHEMA global_temp_tests TO regress_global_temp_user;
+GRANT CREATE ON DATABASE regression TO regress_global_temp_user;
+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 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);
+
+\d tmp1
+\dt+ global_temp_*.tmp*
+
+-- Information schema
+SELECT table_catalog, table_schema, table_name, table_type
+FROM information_schema.tables
+WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
+ORDER BY table_name;
+
+DROP SCHEMA global_temp_xxx CASCADE;
+DROP SCHEMA global_temp_yyy CASCADE;
+
+-- Basic tests
+INSERT INTO tmp1 VALUES (1);
+SELECT * FROM tmp1;
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp1;
+
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+COMMIT;
+SELECT * FROM tmp2;
+DROP TABLE tmp2;
+
+-- ON COMMIT DROP not allowed
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DROP; -- fail
+
+-- Two-phase commit not allowed with global temp tables
+BEGIN;
+SELECT * FROM tmp1;
+PREPARE TRANSACTION 'twophase'; -- fail
+
+-- Test partitioned global temp table
+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 TABLE tmp2_p2 PARTITION OF tmp2 FOR VALUES IN (2);
+CREATE TEMP TABLE local_tmp (a int);
+ALTER TABLE tmp2 ATTACH PARTITION local_tmp FOR VALUES IN (3); -- fail
+INSERT INTO tmp2 VALUES (1), (2);
+SELECT * FROM tmp2 ORDER BY a;
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp2 ORDER BY a;
+DROP TABLE tmp2;
+
+-- Test ALTER TABLE with rewrite
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+ALTER TABLE tmp2 ALTER COLUMN a SET DATA TYPE numeric;
+SELECT a, pg_typeof(a) FROM tmp2;
+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 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;
+
+-- Test ALTER TABLE ... SET TABLESPACE
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+DROP TABLE tmp2;
+
+-- Test dependency on tablespace
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int) TABLESPACE temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE temp_test_tablespace; -- fail
+DROP TABLE tmp2;
+DROP TABLESPACE temp_test_tablespace;
+
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+ALTER TABLE tmp2 SET TABLESPACE temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE temp_test_tablespace; -- fail
+DROP TABLE tmp2;
+DROP TABLESPACE temp_test_tablespace;
+
+-- Test TRUNCATE
+INSERT INTO tmp1 VALUES (1);
+BEGIN;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ROLLBACK;
+SELECT * FROM tmp1;
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+RELEASE sp1;
+SELECT * FROM tmp1;
+ROLLBACK;
+SELECT * FROM tmp1;
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ROLLBACK TO sp1;
+SELECT * FROM tmp1;
+COMMIT;
+SELECT * FROM tmp1;
+
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+
+-- Test view creation
+INSERT INTO tmp1 VALUES (1);
+CREATE VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+DROP VIEW v;
+
+CREATE TEMP VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+DROP VIEW v;
+
+CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 95d5b6e0915..b1f0a60abea 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -367,7 +367,7 @@ ORDER BY 1;
 SELECT c1.oid, c1.relname
 FROM pg_class as c1
 WHERE relkind NOT IN ('r', 'i', 'S', 't', 'v', 'm', 'c', 'f', 'p', 'I') OR
-    relpersistence NOT IN ('p', 'u', 't') OR
+    relpersistence NOT IN ('p', 'u', 't', 'g') OR
     relreplident NOT IN ('d', 'n', 'f', 'i');
 
 -- All tables, indexes, partitioned indexes and matviews should have an
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..4b7798b0f9d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1158,6 +1158,7 @@ GistTsVectorOptions
 GistVacState
 GlobalChannelEntry
 GlobalChannelKey
+GlobalTempRelShmemControl
 GlobalTransaction
 GlobalTransactionData
 GlobalVisHorizonKind
@@ -1187,6 +1188,10 @@ GroupingSet
 GroupingSetData
 GroupingSetKind
 GroupingSetsPath
+GtrStorageEntry
+GtrSharedUsageEntry
+GtrSharedUsageKey
+GtrUsageEntry
 GucAction
 GucBoolAssignHook
 GucBoolCheckHook
-- 
2.51.0

