From 8fdcd3afaf5b75dc3cf31bcbd3943cffcb530879 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Wed, 10 Jun 2026 18:23:24 +0100
Subject: [PATCH v1 4/9] Support global temporary catalog tables and add
 pg_temp_class.

This commit allows system catalog tables to be global temporary
tables, and adds the first such example: pg_temp_class. The idea is
that pg_temp_class will contain one row for each global temporary
table accessed in the session, allowing a subset of the attributes
from pg_class to be overridden locally for that session.

To avoid bootstrapping difficulties when pg_temp_class itself is first
accessed, and needs to insert rows describing itself and its index,
all inserts to pg_temp_class are held in a pending queue to be flushed
later (at the end of the current command or (sub)transaction commit).

In this initial commit, pg_temp_class only has oid, relfilenode, and
reltablespace attributes, allowing CLUSTER, REINDEX, REPACK, TRUNCATE,
and VACUUM FULL to make changes locally to the current session,
without affecting other running sessions. ALTER TABLE SET TABLESPACE
works similarly, except that it updates both pg_class and
pg_temp_class, so that it applies to the current session and any
future sessions, but not any other current sessions that have already
accessed the table.
---
 src/backend/access/transam/xact.c           |  13 +
 src/backend/bootstrap/bootparse.y           |  23 +-
 src/backend/bootstrap/bootscanner.l         |   1 +
 src/backend/catalog/Catalog.pm              |   2 +
 src/backend/catalog/Makefile                |   1 +
 src/backend/catalog/genbki.pl               |  42 ++
 src/backend/catalog/global_temp.c           |  23 +-
 src/backend/catalog/index.c                 |  18 +
 src/backend/catalog/meson.build             |   1 +
 src/backend/catalog/pg_temp_class.c         | 595 ++++++++++++++++++++
 src/backend/commands/repack.c               |  82 ++-
 src/backend/commands/tablecmds.c            |  40 +-
 src/backend/commands/vacuum.c               |  18 +
 src/backend/parser/parse_utilcmd.c          |   9 +-
 src/backend/utils/activity/pgstat_io.c      |  11 +-
 src/backend/utils/adt/dbsize.c              |  11 +-
 src/backend/utils/cache/inval.c             |  31 +-
 src/backend/utils/cache/lsyscache.c         |  14 +
 src/backend/utils/cache/relcache.c          |  69 ++-
 src/backend/utils/cache/syscache.c          |   7 +-
 src/include/access/htup_details.h           |  11 +
 src/include/catalog/Makefile                |   3 +-
 src/include/catalog/genbki.h                |   1 +
 src/include/catalog/meson.build             |   1 +
 src/include/catalog/pg_temp_class.h         | 148 +++++
 src/test/isolation/expected/global-temp.out | 109 ++++
 src/test/isolation/specs/global-temp.spec   |  37 ++
 src/test/recovery/t/018_wal_optimize.pl     |   1 +
 src/test/regress/expected/global_temp.out   | 114 +++-
 src/test/regress/expected/oidjoins.out      |   2 +
 src/test/regress/expected/stats.out         |   3 +-
 src/test/regress/sql/global_temp.sql        |  78 ++-
 src/tools/pgindent/typedefs.list            |   3 +
 33 files changed, 1454 insertions(+), 68 deletions(-)
 create mode 100644 src/backend/catalog/pg_temp_class.c
 create mode 100644 src/include/catalog/pg_temp_class.h

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 5d1b3555f12..21cb4af577d 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/index.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_enum.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
 #include "commands/tablecmds.h"
@@ -1148,6 +1149,9 @@ CommandCounterIncrement(void)
 					(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 					 errmsg("cannot start commands during a parallel operation")));
 
+		/* Flush out any pending inserts to pg_temp_class */
+		PreCCI_PgTempClass();
+
 		currentCommandId += 1;
 		if (currentCommandId == InvalidCommandId)
 		{
@@ -2360,6 +2364,9 @@ CommitTransaction(void)
 	 */
 	PreCommit_GlobalTempRelation();
 
+	/* Flush out any pending inserts to pg_temp_class */
+	PreCommit_PgTempClass();
+
 	/*
 	 * Synchronize files that are created and not WAL-logged during this
 	 * transaction. This must happen before AtEOXact_RelationMap(), so that we
@@ -2632,6 +2639,9 @@ PrepareTransaction(void)
 	 */
 	PreCommit_GlobalTempRelation();
 
+	/* Flush out any pending inserts to pg_temp_class */
+	PreCommit_PgTempClass();
+
 	/*
 	 * Synchronize files that are created and not WAL-logged during this
 	 * transaction. This must happen before EndPrepare(), so that we don't see
@@ -5189,6 +5199,9 @@ CommitSubTransaction(void)
 	CallSubXactCallbacks(SUBXACT_EVENT_PRE_COMMIT_SUB, s->subTransactionId,
 						 s->parent->subTransactionId);
 
+	/* Flush out any pending inserts to pg_temp_class */
+	PreSubCommit_PgTempClass();
+
 	/*
 	 * If this subxact has started any unfinished parallel operation, clean up
 	 * its workers and exit parallel mode.  Warn about leaked resources.
diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 943ff4733d3..7f233248b21 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -96,7 +96,7 @@ static int num_columns_read = 0;
 %type <list>  boot_index_params
 %type <ielem> boot_index_param
 %type <str>   boot_ident
-%type <ival>  optbootstrap optsharedrelation boot_column_nullness
+%type <ival>  optbootstrap optsharedrelation opttemprelation boot_column_nullness
 %type <oidval> oidspec optrowtypeoid
 
 %token <str> ID
@@ -106,7 +106,7 @@ static int num_columns_read = 0;
 /* All the rest are unreserved, and should be handled in boot_ident! */
 %token <kw> OPEN XCLOSE XCREATE INSERT_TUPLE
 %token <kw> XDECLARE INDEX ON USING XBUILD INDICES UNIQUE XTOAST
-%token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XROWTYPE_OID
+%token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XTEMP_RELATION XROWTYPE_OID
 %token <kw> XFORCE XNOT XNULL
 
 %start TopLevel
@@ -155,13 +155,14 @@ Boot_CloseStmt:
 		;
 
 Boot_CreateStmt:
-		  XCREATE boot_ident oidspec optbootstrap optsharedrelation optrowtypeoid LPAREN
+		  XCREATE boot_ident oidspec optbootstrap optsharedrelation opttemprelation optrowtypeoid LPAREN
 				{
 					do_start();
 					numattr = 0;
-					elog(DEBUG4, "creating%s%s relation %s %u",
+					elog(DEBUG4, "creating%s%s%s relation %s %u",
 						 $4 ? " bootstrap" : "",
 						 $5 ? " shared" : "",
+						 $6 ? " global temp" : "",
 						 $2,
 						 $3);
 				}
@@ -173,6 +174,7 @@ Boot_CreateStmt:
 				{
 					TupleDesc	tupdesc;
 					bool		shared_relation;
+					bool		temp_relation;
 					bool		mapped_relation;
 
 					do_start();
@@ -180,6 +182,7 @@ Boot_CreateStmt:
 					tupdesc = CreateTupleDesc(numattr, attrtypes);
 
 					shared_relation = $5;
+					temp_relation = $6;
 
 					/*
 					 * The catalogs that use the relation mapper are the
@@ -211,6 +214,8 @@ Boot_CreateStmt:
 												   HEAP_TABLE_AM_OID,
 												   tupdesc,
 												   RELKIND_RELATION,
+												   temp_relation ?
+												   RELPERSISTENCE_GLOBAL_TEMP :
 												   RELPERSISTENCE_PERMANENT,
 												   shared_relation,
 												   mapped_relation,
@@ -228,13 +233,15 @@ Boot_CreateStmt:
 													  PG_CATALOG_NAMESPACE,
 													  shared_relation ? GLOBALTABLESPACE_OID : 0,
 													  $3,
-													  $6,
+													  $7,
 													  InvalidOid,
 													  BOOTSTRAP_SUPERUSERID,
 													  HEAP_TABLE_AM_OID,
 													  tupdesc,
 													  NIL,
 													  RELKIND_RELATION,
+													  temp_relation ?
+													  RELPERSISTENCE_GLOBAL_TEMP :
 													  RELPERSISTENCE_PERMANENT,
 													  shared_relation,
 													  mapped_relation,
@@ -432,6 +439,11 @@ optsharedrelation:
 		|						{ $$ = 0; }
 		;
 
+opttemprelation:
+			XTEMP_RELATION	{ $$ = 1; }
+		|					{ $$ = 0; }
+		;
+
 optrowtypeoid:
 			XROWTYPE_OID oidspec	{ $$ = $2; }
 		|							{ $$ = InvalidOid; }
@@ -491,6 +503,7 @@ boot_ident:
 		| OBJ_ID		{ $$ = pstrdup($1); }
 		| XBOOTSTRAP	{ $$ = pstrdup($1); }
 		| XSHARED_RELATION	{ $$ = pstrdup($1); }
+		| XTEMP_RELATION	{ $$ = pstrdup($1); }
 		| XROWTYPE_OID	{ $$ = pstrdup($1); }
 		| XFORCE		{ $$ = pstrdup($1); }
 		| XNOT			{ $$ = pstrdup($1); }
diff --git a/src/backend/bootstrap/bootscanner.l b/src/backend/bootstrap/bootscanner.l
index 9674f2795d1..f8c1a671712 100644
--- a/src/backend/bootstrap/bootscanner.l
+++ b/src/backend/bootstrap/bootscanner.l
@@ -82,6 +82,7 @@ create			{ yylval->kw = "create"; return XCREATE; }
 OID				{ yylval->kw = "OID"; return OBJ_ID; }
 bootstrap		{ yylval->kw = "bootstrap"; return XBOOTSTRAP; }
 shared_relation	{ yylval->kw = "shared_relation"; return XSHARED_RELATION; }
+temp_relation	{ yylval->kw = "temp_relation"; return XTEMP_RELATION; }
 rowtype_oid		{ yylval->kw = "rowtype_oid"; return XROWTYPE_OID; }
 
 insert			{ yylval->kw = "insert"; return INSERT_TUPLE; }
diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm
index 219af5884d9..78e69b3f0d3 100644
--- a/src/backend/catalog/Catalog.pm
+++ b/src/backend/catalog/Catalog.pm
@@ -176,6 +176,8 @@ sub ParseHeader
 			$catalog{bootstrap} = /BKI_BOOTSTRAP/ ? ' bootstrap' : '';
 			$catalog{shared_relation} =
 			  /BKI_SHARED_RELATION/ ? ' shared_relation' : '';
+			$catalog{temp_relation} =
+			  /BKI_TEMP_RELATION/ ? ' temp_relation' : '';
 			if (/BKI_ROWTYPE_OID\(\s*
 				 (?<rowtype_oid>\d+),\s*
 				 (?<rowtype_oid_macro>\w+)\s*
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 0fb085fd8ee..b13293a933e 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -46,6 +46,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_tablespace.o \
+	pg_temp_class.o \
 	pg_type.o \
 	storage.o \
 	toasting.o
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 86f3135f9c7..9a3f0164962 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -174,6 +174,7 @@ foreach my $header (@ARGV)
 			index_oid_macro => $index->{index_oid_macro},
 			key => $key,
 			nbuckets => $syscache->{syscache_nbuckets},
+			table_is_temp => $catalogs{$tblname}->{temp_relation} eq "" ? 0 : 1,
 		};
 
 		$syscache_catalogs{$catname} = 1;
@@ -518,6 +519,7 @@ EOM
 	# .bki CREATE command for this catalog
 	print $bki "create $catname $catalog->{relation_oid}"
 	  . $catalog->{shared_relation}
+	  . $catalog->{temp_relation}
 	  . $catalog->{bootstrap}
 	  . $catalog->{rowtype_oid_clause};
 
@@ -838,6 +840,46 @@ foreach my $syscache (sort keys %syscaches)
 print $syscache_ids_fh "} SysCacheIdentifier;\n";
 print $syscache_ids_fh "#define SysCacheSize ($last_syscache + 1)\n\n";
 
+# Macro to test if a catalog relation is global temporary
+print $syscache_ids_fh "/* Is the specified catalog relation a global temporary relation? */\n";
+print $syscache_ids_fh "#define IsGlobalTempCatalogRelation(relid) \\\n";
+
+my $num_clauses = 0;
+foreach my $catname (sort keys %catalogs)
+{
+	my $catalog = $catalogs{$catname};
+
+	if ($catalog->{temp_relation})
+	{
+		print $syscache_ids_fh $num_clauses == 0 ? "\t(" : " || \\\n\t ";
+		print $syscache_ids_fh "(relid) == $catalog->{relation_oid_macro}";
+		$num_clauses++;
+
+		foreach my $index (@{ $catalog->{indexing} })
+		{
+			print $syscache_ids_fh " || \\\n\t (relid) == $index->{index_oid_macro}";
+			$num_clauses++;
+		}
+	}
+}
+print $syscache_ids_fh $num_clauses == 0 ? "false\n\n" : ")\n\n";
+
+# Macro to test if a syscache's relation is global temporary
+print $syscache_ids_fh "/* Does the specified SysCache use a global temporary relation? */\n";
+print $syscache_ids_fh "#define SysCacheRelationIsGlobalTemp(cacheId) \\\n";
+
+$num_clauses = 0;
+foreach my $syscache (sort keys %syscaches)
+{
+	if ($syscaches{$syscache}{table_is_temp})
+	{
+		print $syscache_ids_fh $num_clauses == 0 ? "\t(" : " || \\\n\t ";
+		print $syscache_ids_fh "(cacheId) == $syscache";
+		$num_clauses++;
+	}
+}
+print $syscache_ids_fh $num_clauses == 0 ? "false\n\n" : ")\n\n";
+
 # Closing boilerplate for syscache_ids.h
 print $syscache_ids_fh "#endif\t\t\t\t\t\t\t/* SYSCACHE_IDS_H */\n";
 
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 07980fee4cd..927b8463195 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -60,6 +60,7 @@
 #include "access/xact.h"
 #include "access/xlogutils.h"
 #include "catalog/global_temp.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/storage.h"
 #include "commands/sequence.h"
 #include "lib/dshash.h"
@@ -938,8 +939,17 @@ InvalidateGlobalTempRelation(Oid relid)
 void
 GlobalTempRelationCreated(Relation relation)
 {
-	/* Record our use of the relation */
-	gtr_record_usage(relation->rd_id);
+	/*
+	 * If this is the first time we've used this relation in this session,
+	 * insert a pg_temp_class tuple for it, and update the usage hash tables.
+	 */
+	if (gtr_local_usage == NULL ||
+		hash_search(gtr_local_usage,
+					&relation->rd_id, HASH_FIND, NULL) == NULL)
+	{
+		InsertPgTempClassTuple(relation);
+		gtr_record_usage(relation->rd_id);
+	}
 }
 
 /*
@@ -968,6 +978,9 @@ GlobalTempRelationDropped(Oid relid)
 
 		/* Flag the usage entry for eoxact cleanup */
 		EOXactUsageListAdd(relid);
+
+		/* Delete it's pg_temp_class tuple */
+		DeletePgTempClassTuple(relid);
 	}
 }
 
@@ -1094,6 +1107,9 @@ AtEOXact_GlobalTempRelation(bool isCommit)
 		}
 	}
 
+	/* Perform any pg_temp_class processing */
+	AtEOXact_PgTempClass(isCommit);
+
 	/* Now we're out of the transaction and can clear the lists */
 	eoxact_storage_list_len = 0;
 	eoxact_storage_list_overflowed = false;
@@ -1163,6 +1179,9 @@ AtEOSubXact_GlobalTempRelation(bool isCommit, SubTransactionId mySubid,
 		}
 	}
 
+	/* Perform any pg_temp_class processing */
+	AtEOSubXact_PgTempClass(isCommit, mySubid, parentSubid);
+
 	/* Don't reset the lists; we still need more cleanup later */
 }
 
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9d37a773b46..28646029f16 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -3645,6 +3646,23 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
 
 	pg_rusage_init(&ru0);
 
+	/*
+	 * Special case: cannot recreate pg_temp_class_oid_index --- to do so
+	 * would require pg_temp_class to be a mapped relation (to avoid use of
+	 * the index while rebuilding it) and the relmapper does not support
+	 * temporary tables.  It might be possible to make this work, but it
+	 * doesn't seem worth the effort, so just punt.
+	 */
+	if (indexId == TempClassOidIndexId)
+	{
+		ereport(NOTICE,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot reindex temporary system index \"%s\", skipping",
+					   get_rel_name(indexId)));
+		RemoveReindexPending(indexId);
+		return;
+	}
+
 	/*
 	 * Open and lock the parent heap relation.  ShareLock is sufficient since
 	 * we only need to be sure no schema or data changes are going on.
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 7285ab2dfcf..5386d960b40 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -33,6 +33,7 @@ backend_sources += files(
   'pg_shdepend.c',
   'pg_subscription.c',
   'pg_tablespace.c',
+  'pg_temp_class.c',
   'pg_type.c',
   'storage.c',
   'toasting.c',
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
new file mode 100644
index 00000000000..378c23d3e45
--- /dev/null
+++ b/src/backend/catalog/pg_temp_class.c
@@ -0,0 +1,595 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_class.c
+ *	  routines to support manipulation of the pg_temp_class relation
+ *
+ * The pg_temp_class system catalog table is a global temporary table that
+ * stores local overrides to various fields from the pg_class table for the
+ * duration of the current session.  Currently, this is only used for
+ * global temporary relations, though in the future, it might also be used
+ * for local temporary relations.
+ *
+ * Tuples are first added to pg_temp_class when global temporary relations
+ * (including pg_temp_class itself) are created or opened for the first
+ * time in a session.  This "first time" might be repeated if a previous
+ * "first time" insert was rolled back.
+ *
+ * All tuples to be inserted into pg_temp_class are held in a "pending"
+ * queue, rather than being written out immediately, delaying the point at
+ * which the tuple for pg_temp_class itself is inserted until after the
+ * relation has been fully opened.  This pending queue also serves a number
+ * of other useful purposes --- it prevents system catalog updates in what
+ * might otherwise be expected to be read-only contexts (for example, while
+ * opening a global temporary relation at parse time);  it allows new
+ * pg_temp_class tuples to be seen in the current command, without having
+ * to issue a CommandCounterIncrement(); and it allows us to delay opening
+ * (and hence creating storage for) pg_temp_class itself, until we have to.
+ *
+ * Pending inserts to pg_temp_class are flushed to the database at the end
+ * of any non-read-only command and when committing a transaction or
+ * subtransaction.  They are also flushed out when we notice that the
+ * transaction nesting level has increased, so that the all pending inserts
+ * are for the current subtransaction, and can be safely discarded on
+ * subrollback.
+ *
+ * NB: All reads and writes to pg_temp_class by backend code must go
+ * through the functions defined here (though user SQL queries may read it
+ * normally).
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/catalog/pg_temp_class.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/xact.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_temp_class.h"
+#include "miscadmin.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/*
+ * Have we opened and initialized pg_temp_class in this session?
+ */
+static bool pg_temp_class_opened = false;
+
+/*
+ * Subtransaction ID in which pg_temp_class was opened, if it was opened in
+ * the current transaction, else zero.
+ */
+static SubTransactionId pg_temp_class_subid = InvalidSubTransactionId;
+
+/* Cached copy of the pg_temp_class tuple descriptor */
+static TupleDesc pg_temp_class_tupdesc = NULL;
+
+/* Pending inserts to pg_temp_class */
+typedef struct PendingInsert
+{
+	Oid			relid;			/* lookup key: OID the tuple is for */
+	HeapTuple	tuple;			/* copy of tuple to be inserted */
+} PendingInsert;
+
+static HTAB *pending_inserts = NULL;
+static bool have_pending_inserts = false;
+
+/* Memory context for all tuples pending insert */
+static MemoryContext pending_inserts_tupctx = NULL;
+
+/* Transaction nesting level the pending inserts are for */
+static int	pending_inserts_nest_level = 1;
+
+/*
+ * init_pending_inserts_hashtable
+ *
+ *	Initialize the pending inserts hashtable, if not already done.
+ */
+static void
+init_pending_inserts_hashtable(void)
+{
+	if (pending_inserts == NULL)
+	{
+		HASHCTL		ctl;
+
+		/* Create the hash table */
+		ctl.keysize = sizeof(Oid);
+		ctl.entrysize = sizeof(PendingInsert);
+
+		pending_inserts = hash_create("Pending pg_temp_class inserts",
+									  128, &ctl, HASH_ELEM | HASH_BLOBS);
+
+		/* Create a separate memory context for all tuples in it */
+		pending_inserts_tupctx = AllocSetContextCreate(TopMemoryContext,
+													   "Pending pg_temp_class tuples",
+													   ALLOCSET_DEFAULT_SIZES);
+	}
+}
+
+/*
+ * get_pg_temp_class_tupdesc
+ *
+ *	Returns the tuple descriptor for pg_temp_class.
+ */
+static TupleDesc
+get_pg_temp_class_tupdesc()
+{
+	/* Build the tuple descriptor the first time through */
+	if (pg_temp_class_tupdesc == NULL)
+	{
+		MemoryContext oldcontext;
+		TupleDesc	tupdesc;
+
+		oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+		tupdesc = CreateTemplateTupleDesc(Natts_pg_temp_class);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_oid,
+						   "oid", OIDOID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relfilenode,
+						   "relfilenode", OIDOID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_reltablespace,
+						   "reltablespace", OIDOID, -1, 0);
+		TupleDescFinalize(tupdesc);
+
+		MemoryContextSwitchTo(oldcontext);
+
+		/* Cache it for all future use */
+		pg_temp_class_tupdesc = tupdesc;
+	}
+	return pg_temp_class_tupdesc;
+}
+
+/*
+ * heap_form_pg_temp_class_tuple
+ *
+ *	Create a pg_temp_class tuple for the specified relation.  All tuple data
+ *	is taken from rel->rd_rel.
+ */
+static HeapTuple
+heap_form_pg_temp_class_tuple(Relation rel)
+{
+	Form_pg_class form = rel->rd_rel;
+	Datum		values[Natts_pg_temp_class];
+	bool		nulls[Natts_pg_temp_class] = {0};
+
+	values[Anum_pg_temp_class_oid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
+	values[Anum_pg_temp_class_relfilenode - 1] = ObjectIdGetDatum(form->relfilenode);
+	values[Anum_pg_temp_class_reltablespace - 1] = ObjectIdGetDatum(form->reltablespace);
+
+	return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
+}
+
+/*
+ * flush_pending_pg_temp_class_inserts
+ *
+ *	Flush any pending inserts to pg_temp_class.
+ */
+static void
+flush_pending_pg_temp_class_inserts(void)
+{
+	Relation	pg_temp_class;
+	CatalogIndexState indstate;
+	HASH_SEQ_STATUS status;
+	PendingInsert *entry;
+
+	/*
+	 * Open pg_temp_class, and make note of the subtransaction ID, if this is
+	 * the first time.
+	 */
+	pg_temp_class = table_open(TempRelationRelationId, RowExclusiveLock);
+	if (!pg_temp_class_opened)
+	{
+		pg_temp_class_opened = true;
+		pg_temp_class_subid = GetCurrentSubTransactionId();
+	}
+
+	/* Flush all pending inserts */
+	indstate = CatalogOpenIndexes(pg_temp_class);
+	hash_seq_init(&status, pending_inserts);
+	while ((entry = hash_seq_search(&status)) != NULL)
+	{
+		CatalogTupleInsertWithInfo(pg_temp_class, entry->tuple, indstate);
+		hash_search(pending_inserts, &entry->relid, HASH_REMOVE, NULL);
+	}
+	CatalogCloseIndexes(indstate);
+	table_close(pg_temp_class, RowExclusiveLock);
+
+	/* Should be left with no pending inserts */
+	Assert(hash_get_num_entries(pending_inserts) == 0);
+	have_pending_inserts = false;
+
+	/* Free all memory allocated for tuples */
+	MemoryContextReset(pending_inserts_tupctx);
+}
+
+/*
+ * discard_pending_pg_temp_class_inserts
+ *
+ *	Discard any pending inserts to pg_temp_class.
+ */
+static void
+discard_pending_pg_temp_class_inserts(void)
+{
+	/* Just blow away the hash table and tuple memory context */
+	hash_destroy(pending_inserts);
+	MemoryContextDelete(pending_inserts_tupctx);
+
+	pending_inserts = NULL;
+	pending_inserts_tupctx = NULL;
+	have_pending_inserts = false;
+}
+
+/*
+ * GetPgTempClassTuple
+ *
+ *	Get the pg_temp_class tuple for a global temporary relation.
+ *
+ *	Returns NULL if the tuple could not be found.  Otherwise, the tuple
+ *	returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgTempClassTuple(Oid relid)
+{
+	PendingInsert *entry;
+
+	/* If there is a pending insert for this relation, just return that */
+	if (have_pending_inserts)
+	{
+		entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+		if (entry != NULL)
+			return heap_copytuple(entry->tuple);
+	}
+
+	/* If we haven't opened pg_temp_class yet, it must be empty */
+	if (!pg_temp_class_opened)
+		return NULL;
+
+	/* Otherwise fetch a copy of the tuple from the system caches */
+	return SearchSysCacheCopy1(TEMPRELOID, ObjectIdGetDatum(relid));
+}
+
+/*
+ * InsertPgTempClassTuple
+ *
+ *	Insert a new pg_temp_class tuple for a global temporary relation.
+ *
+ *	This is called when a global temporary relation is created or accessed for
+ *	the first time in a session.  All tuple data is taken from rel->rd_rel.
+ *
+ *	Note: The new tuple is not written to the database unless and until
+ *	CommandCounterIncrement() is called for a non-read-only command, or the
+ *	(sub)transaction is committed, or a new subtranction is started.  However,
+ *	the new tuple *is* visible to Get/Update/DeletePgTempClassTuple() and
+ *	GetEffectivePgClassTuple().
+ */
+void
+InsertPgTempClassTuple(Relation rel)
+{
+	int			nest_level = GetCurrentTransactionNestLevel();
+	Oid			relid = RelationGetRelid(rel);
+	PendingInsert *entry;
+	bool		found;
+	MemoryContext oldcontext;
+
+	/*
+	 * If the transaction nesting level has increased, flush all previous
+	 * pending inserts, so that all new pending inserts are for the same
+	 * subtransaction.  The nesting level should never decrease without us
+	 * knowing about it via AtEOSubXact_PgTempClass().
+	 */
+	Assert(nest_level >= pending_inserts_nest_level);
+	if (nest_level > pending_inserts_nest_level)
+	{
+		if (have_pending_inserts)
+			flush_pending_pg_temp_class_inserts();
+		pending_inserts_nest_level = nest_level;
+	}
+
+	/*
+	 * Add a new tuple for the relation to the pending inserts hash table,
+	 * taking care to allocate the tuple in the long-term memory context for
+	 * pending insert tuples.
+	 */
+	init_pending_inserts_hashtable();
+
+	entry = hash_search(pending_inserts, &relid, HASH_ENTER, &found);
+	if (found)
+		elog(ERROR, "pg_temp_class tuple for relation %u already exists", relid);
+
+	oldcontext = MemoryContextSwitchTo(pending_inserts_tupctx);
+	entry->tuple = heap_form_pg_temp_class_tuple(rel);
+	MemoryContextSwitchTo(oldcontext);
+
+	have_pending_inserts = true;
+}
+
+/*
+ * UpdatePgTempClassTuple
+ *
+ *	Update the pg_temp_class tuple for a global temporary relation.
+ */
+void
+UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple)
+{
+	Relation	pg_temp_class;
+	HeapTuple	oldtuple;
+
+	/* If there is a pending insert for this relation, just update that */
+	if (have_pending_inserts)
+	{
+		PendingInsert *entry;
+		Form_pg_temp_class old_form;
+		Form_pg_temp_class new_form;
+
+		entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+		if (entry != NULL)
+		{
+			old_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
+			new_form = (Form_pg_temp_class) GETSTRUCT(newtuple);
+			COPY_PG_TEMP_CLASS_ATTRS(new_form, old_form);
+
+			return;
+		}
+	}
+
+	/*
+	 * Otherwise, update pg_temp_class directly, making note of the
+	 * subtransaction ID, if this is the first time opening pg_temp_class.
+	 */
+	pg_temp_class = table_open(TempRelationRelationId, RowExclusiveLock);
+	if (!pg_temp_class_opened)
+	{
+		pg_temp_class_opened = true;
+		pg_temp_class_subid = GetCurrentSubTransactionId();
+	}
+
+	oldtuple = SearchSysCache1(TEMPRELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(oldtuple))
+		elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+	CatalogTupleUpdate(pg_temp_class, &oldtuple->t_self, newtuple);
+	ReleaseSysCache(oldtuple);
+
+	table_close(pg_temp_class, RowExclusiveLock);
+}
+
+/*
+ * DeletePgTempClassTuple
+ *
+ *	Delete the pg_temp_class tuple for a global temporary relation.
+ */
+void
+DeletePgTempClassTuple(Oid relid)
+{
+	Relation	pg_temp_class;
+	HeapTuple	oldtuple;
+
+	/* If there is a pending insert for this relation, just delete that */
+	if (have_pending_inserts)
+	{
+		PendingInsert *entry;
+
+		entry = hash_search(pending_inserts, &relid, HASH_REMOVE, NULL);
+		if (entry != NULL)
+		{
+			heap_freetuple(entry->tuple);
+			return;
+		}
+	}
+
+	/*
+	 * Otherwise, update pg_temp_class directly, making note of the
+	 * subtransaction ID, if this is the first time opening pg_temp_class.
+	 */
+	pg_temp_class = table_open(TempRelationRelationId, RowExclusiveLock);
+	if (!pg_temp_class_opened)
+	{
+		pg_temp_class_opened = true;
+		pg_temp_class_subid = GetCurrentSubTransactionId();
+	}
+
+	oldtuple = SearchSysCache1(TEMPRELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(oldtuple))
+		elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+	CatalogTupleDelete(pg_temp_class, &oldtuple->t_self);
+	ReleaseSysCache(oldtuple);
+
+	table_close(pg_temp_class, RowExclusiveLock);
+}
+
+/*
+ * GetPgClassAndPgTempClassTuples
+ *
+ *	Get the pg_class tuple for a relation, and if it's a global temporary
+ *	relation, also get the corresponding pg_temp_class tuple.
+ *
+ *	If lock_tuple is true, the pg_class tuple will be locked, but not the
+ *	pg_temp_class tuple.
+ *
+ *	If check_temp is true, an error will be raised if a global temporary
+ *	relation's pg_temp_class tuple is not found.  After a global temporary
+ *	relation has been opened, its pg_temp_class tuple should always exist.
+ *
+ *	Returns NULL if the pg_class tuple could not be found.  Otherwise, the
+ *	tuple(s) returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
+							   HeapTuple *temp_tuple, bool check_temp)
+{
+	HeapTuple	tuple;
+
+	/* Get a copy of the pg_class tuple */
+	if (lock_tuple)
+		tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(relid));
+	else
+		tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+
+	if (HeapTupleIsValid(tuple) &&
+		((Form_pg_class) GETSTRUCT(tuple))->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		/* Get the pg_temp_class tuple, and check it exists, if requested */
+		*temp_tuple = GetPgTempClassTuple(relid);
+		if (check_temp && !HeapTupleIsValid(*temp_tuple))
+			elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+	}
+	else
+		*temp_tuple = NULL;
+
+	return tuple;
+}
+
+/*
+ * GetEffectivePgClassTuple
+ *
+ *	Get the effective pg_class tuple for a relation.
+ *
+ *	This will fetch the pg_class tuple for the relation and then, if it's a
+ *	global temporary relation, fetch the corresponding pg_temp_class tuple and
+ *	use the values in it to override the corresponding values in the pg_class
+ *	tuple.  Thus, the result represents the effective state of the relation in
+ *	this session.
+ *
+ *	For a global temporary relation that has not yet been opened in this
+ *	session, there will be no pg_temp_class tuple, and the pg_class tuple will
+ *	be returned unchanged.
+ *
+ *	Returns NULL if the pg_class tuple could not be found.  Otherwise, the
+ *	tuple returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetEffectivePgClassTuple(Oid relid)
+{
+	HeapTuple	tuple;
+	HeapTuple	temp_tuple;
+	Form_pg_class classform;
+	Form_pg_temp_class temp_classform;
+
+	/*
+	 * Get the pg_class and pg_temp_class tuples.  If we have the latter, use
+	 * it to update the former.
+	 */
+	tuple = GetPgClassAndPgTempClassTuples(relid, false, &temp_tuple, false);
+
+	if (HeapTupleIsValid(tuple) && HeapTupleIsValid(temp_tuple))
+	{
+		classform = (Form_pg_class) GETSTRUCT(tuple);
+		temp_classform = (Form_pg_temp_class) GETSTRUCT(temp_tuple);
+		COPY_PG_TEMP_CLASS_ATTRS(temp_classform, classform);
+	}
+	return tuple;
+}
+
+/*
+ * PreCCI_PgTempClass
+ *
+ *	Pre-end-of-command processing; flush out any pending inserts.
+ */
+void
+PreCCI_PgTempClass(void)
+{
+	if (have_pending_inserts)
+		flush_pending_pg_temp_class_inserts();
+}
+
+/*
+ * PreCommit_PgTempClass
+ *
+ *	Pre-commit processing; flush out any pending inserts.
+ */
+void
+PreCommit_PgTempClass(void)
+{
+	if (have_pending_inserts)
+		flush_pending_pg_temp_class_inserts();
+}
+
+/*
+ * PreSubCommit_PgTempClass
+ *
+ *	Pre-subcommit processing; flush out any pending inserts.
+ */
+void
+PreSubCommit_PgTempClass(void)
+{
+	if (have_pending_inserts)
+		flush_pending_pg_temp_class_inserts();
+}
+
+/*
+ * AtEOXact_PgTempClass
+ *
+ *	Main-transaction commit or abort processing.
+ */
+void
+AtEOXact_PgTempClass(bool isCommit)
+{
+	/*
+	 * If pg_temp_class was first opened and initialized in this transaction,
+	 * rollback undoes that, and it is no longer initialized.
+	 */
+	if (!isCommit && pg_temp_class_subid != InvalidSubTransactionId)
+		pg_temp_class_opened = false;
+	pg_temp_class_subid = InvalidSubTransactionId;
+
+	/* On rollback, discard any pending inserts */
+	if (!isCommit && have_pending_inserts)
+		discard_pending_pg_temp_class_inserts();
+
+	/*
+	 * Reset the pending inserts transaction nesting level so that inserts are
+	 * flushed if a new subtransaction is started, but not a new top-level
+	 * transaction.
+	 */
+	pending_inserts_nest_level = 1;
+}
+
+/*
+ * AtEOSubXact_PgTempClass
+ *
+ *	Sub-transaction commit or abort processing.
+ */
+void
+AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
+						SubTransactionId parentSubid)
+{
+	/*
+	 * Was pg_temp_class first opened and initialized in the current
+	 * subtransaction?
+	 *
+	 * During subcommit, mark it was belonging to the parent.  Otherwise, it
+	 * is no longer initialized.
+	 */
+	if (pg_temp_class_subid == mySubid)
+	{
+		if (isCommit)
+			pg_temp_class_subid = parentSubid;
+		else
+		{
+			pg_temp_class_opened = false;
+			pg_temp_class_subid = InvalidSubTransactionId;
+		}
+	}
+
+	/*
+	 * If pending inserts are for this transaction nesting level, and we're
+	 * rolling back, discard them.
+	 */
+	if (!isCommit && have_pending_inserts &&
+		pending_inserts_nest_level == GetCurrentTransactionNestLevel())
+	{
+		discard_pending_pg_temp_class_inserts();
+	}
+	pending_inserts_nest_level--;
+}
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 5b7a02cbd9f..b879709aeb0 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_inherits.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
@@ -1536,9 +1537,13 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 {
 	Relation	relRelation;
 	HeapTuple	reltup1,
-				reltup2;
+				reltup2,
+				temp_reltup1,
+				temp_reltup2;
 	Form_pg_class relform1,
 				relform2;
+	Form_pg_temp_class temp_relform1,
+				temp_relform2;
 	RelFileNumber relfilenumber1,
 				relfilenumber2;
 	RelFileNumber swaptemp;
@@ -1546,21 +1551,28 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 	Oid			relam1,
 				relam2;
 
-	/* We need writable copies of both pg_class tuples. */
+	/*
+	 * We need writable copies of both pg_class tuples, and the corresponding
+	 * pg_temp_class tuples, if they're global temporary relations.
+	 */
 	relRelation = table_open(RelationRelationId, RowExclusiveLock);
 
-	reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r1));
+	reltup1 = GetPgClassAndPgTempClassTuples(r1, false, &temp_reltup1, true);
 	if (!HeapTupleIsValid(reltup1))
 		elog(ERROR, "cache lookup failed for relation %u", r1);
 	relform1 = (Form_pg_class) GETSTRUCT(reltup1);
+	temp_relform1 = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_reltup1);
 
-	reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r2));
+	reltup2 = GetPgClassAndPgTempClassTuples(r2, false, &temp_reltup2, true);
 	if (!HeapTupleIsValid(reltup2))
 		elog(ERROR, "cache lookup failed for relation %u", r2);
 	relform2 = (Form_pg_class) GETSTRUCT(reltup2);
+	temp_relform2 = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_reltup2);
+
+	Assert(HeapTupleIsValid(temp_reltup1) == HeapTupleIsValid(temp_reltup2));
 
-	relfilenumber1 = relform1->relfilenode;
-	relfilenumber2 = relform2->relfilenode;
+	relfilenumber1 = GetEffective_relfilenode(relform1, temp_relform1);
+	relfilenumber2 = GetEffective_relfilenode(relform2, temp_relform2);
 	relam1 = relform1->relam;
 	relam2 = relform2->relam;
 
@@ -1573,13 +1585,14 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 		 */
 		Assert(!target_is_pg_class);
 
-		swaptemp = relform1->relfilenode;
-		relform1->relfilenode = relform2->relfilenode;
-		relform2->relfilenode = swaptemp;
+		SetEffective_relfilenode(relform1, temp_relform1, relfilenumber2);
+		SetEffective_relfilenode(relform2, temp_relform2, relfilenumber1);
 
-		swaptemp = relform1->reltablespace;
-		relform1->reltablespace = relform2->reltablespace;
-		relform2->reltablespace = swaptemp;
+		swaptemp = GetEffective_reltablespace(relform1, temp_relform1);
+		SetEffective_reltablespace(relform1, temp_relform1,
+								   GetEffective_reltablespace(relform2,
+															  temp_relform2));
+		SetEffective_reltablespace(relform2, temp_relform2, swaptemp);
 
 		swaptemp = relform1->relam;
 		relform1->relam = relform2->relam;
@@ -1748,6 +1761,17 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 		CacheInvalidateRelcacheByTuple(reltup2);
 	}
 
+	/*
+	 * For global temporary relations, update the tuples in pg_temp_class.
+	 */
+	if (HeapTupleIsValid(temp_reltup1) && HeapTupleIsValid(temp_reltup2))
+	{
+		UpdatePgTempClassTuple(r1, temp_reltup1);
+		UpdatePgTempClassTuple(r2, temp_reltup2);
+		heap_freetuple(temp_reltup1);
+		heap_freetuple(temp_reltup2);
+	}
+
 	/*
 	 * Now that pg_class has been updated with its relevant information for
 	 * the swap, update the dependency of the relations to point to their new
@@ -2169,6 +2193,16 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt)
 
 			index = (Form_pg_index) GETSTRUCT(tuple);
 
+			/*
+			 * Silently skip pg_temp_class --- it does not support relfilenode
+			 * changes, because that would require it to be a mapped relation,
+			 * and the relmapper does not support temporary tables.  It might
+			 * be possible to make this work, but it doesn't seem worth the
+			 * effort.
+			 */
+			if (index->indrelid == TempRelationRelationId)
+				continue;
+
 			/*
 			 * Try to obtain a light lock on the index's table, to ensure it
 			 * doesn't go away while we collect the list.  If we cannot, just
@@ -2228,6 +2262,16 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt)
 
 			class = (Form_pg_class) GETSTRUCT(tuple);
 
+			/*
+			 * Silently skip pg_temp_class --- it does not support relfilenode
+			 * changes, because that would require it to be a mapped relation,
+			 * and the relmapper does not support temporary tables.  It might
+			 * be possible to make this work, but it doesn't seem worth the
+			 * effort.
+			 */
+			if (class->oid == TempRelationRelationId)
+				continue;
+
 			/*
 			 * Try to obtain a light lock on the table, to ensure it doesn't
 			 * go away while we collect the list.  If we cannot, just
@@ -2420,6 +2464,20 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 				errmsg("cannot execute %s on temporary tables of other sessions",
 					   RepackCommandAsString(stmt->command)));
 
+	/*
+	 * Reject clustering pg_temp_class --- it does not support relfilenode
+	 * changes, because that would require it to be a mapped relation, and the
+	 * relmapper does not support temporary tables.  It might be possible to
+	 * make this work, but it doesn't seem worth the effort.
+	 */
+	if (tableOid == TempRelationRelationId)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+		/*- translator: first %s is name of a SQL command, eg. REPACK */
+				errmsg("cannot execute %s on temporary system catalog \"%s\"",
+					   RepackCommandAsString(stmt->command),
+					   RelationGetRelationName(rel)));
+
 	/*
 	 * For partitioned tables, let caller handle this.  Otherwise, process it
 	 * here and we're done.
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9be6a6d5a2b..09a485e41d2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_rewrite.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
@@ -3804,7 +3805,8 @@ CheckRelationTableSpaceMove(Relation rel, Oid newTableSpaceId)
 
 /*
  * SetRelationTableSpace
- *		Set new reltablespace and relfilenumber in pg_class entry.
+ *		Set new reltablespace and relfilenumber in pg_class (and/or
+ *		pg_temp_class for a global temporary relation).
  *
  * newTableSpaceId is the new tablespace for the relation, and
  * newRelFilenumber its new filenumber.  If newRelFilenumber is
@@ -3824,33 +3826,55 @@ SetRelationTableSpace(Relation rel,
 {
 	Relation	pg_class;
 	HeapTuple	tuple;
+	HeapTuple	temp_tuple;
 	ItemPointerData otid;
 	Form_pg_class rd_rel;
+	Form_pg_temp_class temp_rd_rel;
 	Oid			reloid = RelationGetRelid(rel);
 
 	Assert(CheckRelationTableSpaceMove(rel, newTableSpaceId));
 
-	/* Get a modifiable copy of the relation's pg_class row. */
+	/*
+	 * Get a modifiable copy of the relation's pg_class row and, for a global
+	 * temporary relation, its pg_temp_class row.
+	 */
 	pg_class = table_open(RelationRelationId, RowExclusiveLock);
 
-	tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(reloid));
+	tuple = GetPgClassAndPgTempClassTuples(reloid, true, &temp_tuple, true);
 	if (!HeapTupleIsValid(tuple))
 		elog(ERROR, "cache lookup failed for relation %u", reloid);
 	otid = tuple->t_self;
 	rd_rel = (Form_pg_class) GETSTRUCT(tuple);
+	temp_rd_rel = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_tuple);
 
-	/* Update the pg_class row. */
-	rd_rel->reltablespace = (newTableSpaceId == MyDatabaseTableSpace) ?
-		InvalidOid : newTableSpaceId;
+	/*
+	 * Update the pg_class and/or pg_temp_class rows.  For global temporary
+	 * relations, the new tablespace is set in both pg_class and pg_temp_class
+	 * so that the change is made in the current session and for all future
+	 * sessions.  Other current sessions using the relation are not affected.
+	 */
+	SetEffective_reltablespace(rd_rel, temp_rd_rel,
+							   newTableSpaceId == MyDatabaseTableSpace ?
+							   InvalidOid : newTableSpaceId);
 	if (RelFileNumberIsValid(newRelFilenumber))
-		rd_rel->relfilenode = newRelFilenumber;
+		SetEffective_relfilenode(rd_rel, temp_rd_rel, newRelFilenumber);
+
 	CatalogTupleUpdate(pg_class, &otid, tuple);
 	UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
+	if (HeapTupleIsValid(temp_tuple))
+	{
+		UpdatePgTempClassTuple(reloid, temp_tuple);
+		heap_freetuple(temp_tuple);
+	}
 
 	/*
 	 * Record dependency on tablespace.  This is required for relations that
 	 * have no physical storage, and for global temporary relations whose
-	 * physical storage is temporary.
+	 * physical storage is temporary.  Note that a global temporary relation
+	 * being used in another session will not see the change in tablespace,
+	 * and will continue to use the original tablespace until the session
+	 * exits, but its local temporary storage will prevent the old tablespace
+	 * from being dropped until then.
 	 */
 	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
 		RELATION_IS_GLOBAL_TEMP(rel))
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..d76d51aaef5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -37,6 +37,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_inherits.h"
+#include "catalog/pg_temp_class.h"
 #include "commands/async.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
@@ -2155,6 +2156,23 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		return false;
 	}
 
+	/*
+	 * VACUUM FULL on pg_temp_class is not supported --- it does not support
+	 * relfilenode changes, because that would require it to be a mapped
+	 * relation, and the relmapper does not support temporary tables. It might
+	 * be possible to make this work, but it doesn't seem worth the effort, so
+	 * do an "aggressive" VACUUM FREEZE instead.
+	 */
+	if (relid == TempRelationRelationId && (params.options & VACOPT_FULL))
+	{
+		params.options &= ~VACOPT_FULL;
+		params.options |= VACOPT_FREEZE;
+		params.freeze_min_age = 0;
+		params.freeze_table_age = 0;
+		params.multixact_freeze_min_age = 0;
+		params.multixact_freeze_table_age = 0;
+	}
+
 	/*
 	 * Silently ignore partitioned tables as there is no work to be done.  The
 	 * useful work is on their child partitions, which have been queued up for
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 9fb216752dd..603701e5352 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
@@ -1714,10 +1715,10 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 		*constraintOid = InvalidOid;
 
 	/*
-	 * Fetch pg_class tuple of source index.  We can't use the copy in the
-	 * relcache entry because it doesn't include optional fields.
+	 * Fetch effective pg_class tuple of source index.  We can't use the copy
+	 * in the relcache entry because it doesn't include optional fields.
 	 */
-	ht_idxrel = SearchSysCache1(RELOID, ObjectIdGetDatum(source_relid));
+	ht_idxrel = GetEffectivePgClassTuple(source_relid);
 	if (!HeapTupleIsValid(ht_idxrel))
 		elog(ERROR, "cache lookup failed for relation %u", source_relid);
 	idxrelrec = (Form_pg_class) GETSTRUCT(ht_idxrel);
@@ -2032,7 +2033,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	}
 
 	/* Clean up */
-	ReleaseSysCache(ht_idxrel);
+	heap_freetuple(ht_idxrel);
 	ReleaseSysCache(ht_am);
 
 	return index;
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 13a5d8e6440..d94f20e6ff3 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -421,18 +421,17 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 		return false;
 
 	/*
-	 * In core Postgres, only regular backends and WAL Sender processes
-	 * executing queries will use local buffers and operate on temporary
-	 * relations. Parallel workers will not use local buffers (see
+	 * In core Postgres, only initdb, regular backends, and WAL Sender
+	 * processes executing queries will use local buffers and operate on
+	 * temporary relations. Parallel workers will not use local buffers (see
 	 * InitLocalBuffers()); however, extensions leveraging background workers
 	 * have no such limitation, so track IO on IOOBJECT_TEMP_RELATION for
 	 * BackendType B_BG_WORKER.
 	 */
 	no_temp_rel = bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER ||
 		bktype == B_CHECKPOINTER || bktype == B_AUTOVAC_WORKER ||
-		bktype == B_STANDALONE_BACKEND || bktype == B_STARTUP ||
-		bktype == B_WAL_SUMMARIZER || bktype == B_WAL_WRITER ||
-		bktype == B_WAL_RECEIVER;
+		bktype == B_STARTUP || bktype == B_WAL_SUMMARIZER ||
+		bktype == B_WAL_WRITER || bktype == B_WAL_RECEIVER;
 
 	if (no_temp_rel && io_context == IOCONTEXT_NORMAL &&
 		io_object == IOOBJECT_TEMP_RELATION)
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index e09ea8fe220..df1961accb2 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -19,6 +19,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
 #include "commands/tablespace.h"
 #include "miscadmin.h"
 #include "storage/fd.h"
@@ -901,7 +902,7 @@ pg_relation_filenode(PG_FUNCTION_ARGS)
 	HeapTuple	tuple;
 	Form_pg_class relform;
 
-	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	tuple = GetEffectivePgClassTuple(relid);
 	if (!HeapTupleIsValid(tuple))
 		PG_RETURN_NULL();
 	relform = (Form_pg_class) GETSTRUCT(tuple);
@@ -920,7 +921,7 @@ pg_relation_filenode(PG_FUNCTION_ARGS)
 		result = InvalidRelFileNumber;
 	}
 
-	ReleaseSysCache(tuple);
+	heap_freetuple(tuple);
 
 	if (!RelFileNumberIsValid(result))
 		PG_RETURN_NULL();
@@ -978,7 +979,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 	ProcNumber	backend;
 	RelPathStr	path;
 
-	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	tuple = GetEffectivePgClassTuple(relid);
 	if (!HeapTupleIsValid(tuple))
 		PG_RETURN_NULL();
 	relform = (Form_pg_class) GETSTRUCT(tuple);
@@ -1011,7 +1012,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 
 	if (!RelFileNumberIsValid(rlocator.relNumber))
 	{
-		ReleaseSysCache(tuple);
+		heap_freetuple(tuple);
 		PG_RETURN_NULL();
 	}
 
@@ -1041,7 +1042,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 			break;
 	}
 
-	ReleaseSysCache(tuple);
+	heap_freetuple(tuple);
 
 	path = relpathbackend(rlocator, backend, MAIN_FORKNUM);
 
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 63dc36d4d91..f699fd4966f 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -51,10 +51,11 @@
  *	PrepareToInvalidateCacheTuple() routine provides the knowledge of which
  *	catcaches may need invalidation for a given tuple.
  *
- *	Also, whenever we see an operation on a pg_class, pg_attribute, or
- *	pg_index tuple, we register a relcache flush operation for the relation
- *	described by that tuple (as specified in CacheInvalidateHeapTuple()).
- *	Likewise for pg_constraint tuples for foreign keys on relations.
+ *	Also, whenever we see an operation on a pg_class, pg_temp_class,
+ *	pg_attribute, or pg_index tuple, we register a relcache flush operation
+ *	for the relation described by that tuple (as specified in
+ *	CacheInvalidateHeapTuple()).  Likewise for pg_constraint tuples for
+ *	foreign keys on relations.
  *
  *	We keep the relcache flush requests in lists separate from the catcache
  *	tuple flush requests.  This allows us to issue all the pending catcache
@@ -119,6 +120,7 @@
 #include "access/xloginsert.h"
 #include "catalog/catalog.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_temp_class.h"
 #include "miscadmin.h"
 #include "storage/procnumber.h"
 #include "storage/sinval.h"
@@ -1496,6 +1498,27 @@ CacheInvalidateHeapTupleCommon(Relation relation,
 		else
 			databaseId = MyDatabaseId;
 	}
+	else if (tupleRelId == TempRelationRelationId)
+	{
+		Form_pg_temp_class temp_classtup = (Form_pg_temp_class) GETSTRUCT(tuple);
+
+		/*
+		 * When a pg_temp_class row is updated, we should send out a relcache
+		 * inval for the relation.  However, insert and delete do not require
+		 * an inval --- the former only happens when a global temporary
+		 * relation is created or initialized for the first time, in which
+		 * case we've just built a valid relcache entry; the latter only
+		 * happens when the relation is dropped, which already triggers a
+		 * relcache inval from pg_class.
+		 */
+		if (HeapTupleIsValid(newtuple))
+		{
+			relationId = temp_classtup->oid;
+			databaseId = MyDatabaseId;
+		}
+		else
+			return;
+	}
 	else if (tupleRelId == AttributeRelationId)
 	{
 		Form_pg_attribute atttup = (Form_pg_attribute) GETSTRUCT(tuple);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 036de5f79ef..cbd25fa6144 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_range.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -2368,6 +2369,19 @@ get_rel_tablespace(Oid relid)
 		Oid			result;
 
 		result = reltup->reltablespace;
+
+		/* Global temporary relations may override reltablespace locally */
+		if (reltup->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			HeapTuple	temp_tp;
+
+			temp_tp = GetPgTempClassTuple(relid);
+			if (HeapTupleIsValid(temp_tp))
+			{
+				result = ((Form_pg_temp_class) GETSTRUCT(temp_tp))->reltablespace;
+				heap_freetuple(temp_tp);
+			}
+		}
 		ReleaseSysCache(tp);
 		return result;
 	}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 62bbc710358..52523b14e92 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -61,6 +61,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/schemapg.h"
@@ -336,6 +337,10 @@ static void unlink_initfile(const char *initfilename, int elevel);
  *		an attribute were to be added after scanning pg_class and before
  *		scanning pg_attribute, relnatts wouldn't match.
  *
+ *		If targetRelId is a global temporary relation, pg_temp_class is
+ *		also scanned, and if a matching tuple is found, its attributes are
+ *		used to override the corresponding attributes from pg_class.
+ *
  *		NB: the returned tuple has been copied into palloc'd storage
  *		and must eventually be freed with heap_freetuple.
  */
@@ -403,6 +408,36 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
 
 	table_close(pg_class_desc, AccessShareLock);
 
+	/*
+	 * For global temporary relations, also scan pg_temp_class.  We cannot do
+	 * this for pg_temp_class itself, or its index, because they may not have
+	 * been loaded yet.  That's OK because we only really need relfilenumber
+	 * and reltablespace to be correct at this stage, and we disallow changes
+	 * to those attributes for these relations.
+	 */
+	if (HeapTupleIsValid(pg_class_tuple) &&
+		targetRelId != TempRelationRelationId &&
+		targetRelId != TempClassOidIndexId)
+	{
+		Form_pg_class pg_class_form;
+
+		pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple);
+
+		if (pg_class_form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			HeapTuple	pg_temp_class_tuple;
+			Form_pg_temp_class pg_temp_class_form;
+
+			pg_temp_class_tuple = GetPgTempClassTuple(targetRelId);
+			if (HeapTupleIsValid(pg_temp_class_tuple))
+			{
+				pg_temp_class_form = (Form_pg_temp_class) GETSTRUCT(pg_temp_class_tuple);
+				COPY_PG_TEMP_CLASS_ATTRS(pg_temp_class_form, pg_class_form);
+				heap_freetuple(pg_temp_class_tuple);
+			}
+		}
+	}
+
 	return pg_class_tuple;
 }
 
@@ -3823,7 +3858,9 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	Relation	pg_class;
 	ItemPointerData otid;
 	HeapTuple	tuple;
+	HeapTuple	temp_tuple;
 	Form_pg_class classform;
+	Form_pg_temp_class temp_classform;
 	MultiXactId minmulti = InvalidMultiXactId;
 	TransactionId freezeXid = InvalidTransactionId;
 	RelFileLocator newrlocator;
@@ -3860,17 +3897,19 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 				 errmsg("unexpected request for new relfilenumber in binary upgrade mode")));
 
 	/*
-	 * Get a writable copy of the pg_class tuple for the given relation.
+	 * Get a writable copy of the relation's pg_class tuple and, for a global
+	 * temporary relation, its pg_temp_class tuple.
 	 */
 	pg_class = table_open(RelationRelationId, RowExclusiveLock);
 
-	tuple = SearchSysCacheLockedCopy1(RELOID,
-									  ObjectIdGetDatum(RelationGetRelid(relation)));
+	tuple = GetPgClassAndPgTempClassTuples(RelationGetRelid(relation), true,
+										   &temp_tuple, true);
 	if (!HeapTupleIsValid(tuple))
 		elog(ERROR, "could not find tuple for relation %u",
 			 RelationGetRelid(relation));
 	otid = tuple->t_self;
 	classform = (Form_pg_class) GETSTRUCT(tuple);
+	temp_classform = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_tuple);
 
 	/*
 	 * Schedule unlinking of the old storage at transaction commit, except
@@ -3976,8 +4015,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	}
 	else
 	{
-		/* Normal case, update the pg_class entry */
-		classform->relfilenode = newrelfilenumber;
+		/* Normal case, update the pg_class and pg_temp_class entries */
+		SetEffective_relfilenode(classform, temp_classform, newrelfilenumber);
 
 		/* relpages etc. never change for sequences */
 		if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
@@ -3992,6 +4031,11 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 		classform->relpersistence = persistence;
 
 		CatalogTupleUpdate(pg_class, &otid, tuple);
+		if (HeapTupleIsValid(temp_tuple))
+		{
+			UpdatePgTempClassTuple(RelationGetRelid(relation), temp_tuple);
+			heap_freetuple(temp_tuple);
+		}
 	}
 
 	UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
@@ -4000,8 +4044,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	table_close(pg_class, RowExclusiveLock);
 
 	/*
-	 * Make the pg_class row change or relation map change visible.  This will
-	 * cause the relcache entry to get updated, too.
+	 * Make the pg_class and pg_temp_class row changes or relation map change
+	 * visible.  This will cause the relcache entry to get updated, too.
 	 */
 	CommandCounterIncrement();
 
@@ -6887,6 +6931,15 @@ write_item(const void *data, Size len, FILE *fp)
  * of the latter. The special cases are relations where
  * RelationCacheInitializePhase2/3 chooses to nail for efficiency reasons, but
  * which do not support any syscache.
+ *
+ * Global temporary relations are never nailed (because that would required
+ * them to be mapped, and the relmapper does not support temporary relations),
+ * but they do all support syscaches.  Despite this, we intentionally do not
+ * cache global temporary relations, since we don't want to load them on
+ * startup, because doing so would result in temporary relation storage being
+ * created when it might not be needed.  Instead, all global temporary
+ * relations are lazily initialized, if and when they are needed.  See also
+ * InitCatalogCachePhase2().
  */
 bool
 RelationIdIsInInitFile(Oid relationId)
@@ -6903,6 +6956,8 @@ RelationIdIsInInitFile(Oid relationId)
 		Assert(!RelationSupportsSysCache(relationId));
 		return true;
 	}
+	if (IsGlobalTempCatalogRelation(relationId))
+		return false;
 	return RelationSupportsSysCache(relationId);
 }
 
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index f4233f9e31a..7fe380c2c06 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -176,6 +176,10 @@ InitCatalogCache(void)
  * relcache with entries for the most-commonly-used system catalogs.
  * Therefore, we invoke this routine when we need to write a new relcache
  * init file.
+ *
+ * We skip caches based on global temporary relations because we don't want
+ * temporary relation storage to be needlessly created on startup.  Instead,
+ * always initialize these caches on first use.
  */
 void
 InitCatalogCachePhase2(void)
@@ -185,7 +189,8 @@ InitCatalogCachePhase2(void)
 	Assert(CacheInitialized);
 
 	for (cacheId = 0; cacheId < SysCacheSize; cacheId++)
-		InitCatCachePhase2(SysCache[cacheId], true);
+		if (!SysCacheRelationIsGlobalTemp(cacheId))
+			InitCatCachePhase2(SysCache[cacheId], true);
 }
 
 
diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h
index 77a6c48fd71..4084fad6c6b 100644
--- a/src/include/access/htup_details.h
+++ b/src/include/access/htup_details.h
@@ -721,6 +721,17 @@ GETSTRUCT(const HeapTupleData *tuple)
 	return ((char *) (tuple->t_data) + tuple->t_data->t_hoff);
 }
 
+/*
+ * GETSTRUCT_SAFE - given a possibly NULL HeapTuple pointer, return the
+ * address of the user data or NULL
+ */
+static inline void *
+GETSTRUCT_SAFE(const HeapTupleData *tuple)
+{
+	return HeapTupleIsValid(tuple) ?
+		((char *) (tuple->t_data) + tuple->t_data->t_hoff) : NULL;
+}
+
 /*
  * Accessor functions to be used with HeapTuple pointers.
  */
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index bab57372b88..629a13edc24 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -86,7 +86,8 @@ CATALOG_HEADERS := \
 	pg_propgraph_element_label.h \
 	pg_propgraph_label.h \
 	pg_propgraph_label_property.h \
-	pg_propgraph_property.h
+	pg_propgraph_property.h \
+	pg_temp_class.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/genbki.h b/src/include/catalog/genbki.h
index 12d2a3e295b..2f1253281c5 100644
--- a/src/include/catalog/genbki.h
+++ b/src/include/catalog/genbki.h
@@ -44,6 +44,7 @@
 /* Options that may appear after CATALOG (on the same line) */
 #define BKI_BOOTSTRAP
 #define BKI_SHARED_RELATION
+#define BKI_TEMP_RELATION
 #define BKI_ROWTYPE_OID(oid,oidmacro)
 #define BKI_SCHEMA_MACRO
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index fa836e4ee25..404f8503276 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -74,6 +74,7 @@ catalog_headers = [
   'pg_propgraph_label.h',
   'pg_propgraph_label_property.h',
   'pg_propgraph_property.h',
+  'pg_temp_class.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
new file mode 100644
index 00000000000..c9efd08ec78
--- /dev/null
+++ b/src/include/catalog/pg_temp_class.h
@@ -0,0 +1,148 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_class.h
+ *	  definition of the "temporary relation" system catalog (pg_temp_class)
+ *
+ * This is a global temporary system catalog table storing session-specific
+ * information about temporary relations.  Currently, it is only used for
+ * global temporary relations.  The attributes are a subset of those from
+ * pg_class, and their values take precedence over the values from pg_class.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_temp_class.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_CLASS_H
+#define PG_TEMP_CLASS_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_temp_class_d.h"	/* IWYU pragma: export */
+
+/* ----------------
+ *		pg_temp_class definition.  cpp turns this into
+ *		typedef struct FormData_pg_temp_class
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
+{
+	/* oid */
+	Oid			oid BKI_LOOKUP(pg_class);
+
+	/* identifier of physical storage file */
+	/* relfilenode == 0 means it is a "mapped" relation, see relmapper.c */
+	Oid			relfilenode BKI_DEFAULT(0);
+
+	/* identifier of table space for relation (0 means default for database) */
+	Oid			reltablespace BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_tablespace);
+} FormData_pg_temp_class;
+
+END_CATALOG_STRUCT
+
+/* ----------------
+ *		Form_pg_temp_class corresponds to a pointer to a tuple with
+ *		the format of pg_temp_class relation.
+ * ----------------
+ */
+typedef FormData_pg_temp_class *Form_pg_temp_class;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_class_oid_index, 8083, TempClassOidIndexId, pg_temp_class, btree(oid oid_ops));
+
+MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
+
+/*
+ * Copy all pg_temp_class attributes from "source" to "target", where the
+ * source and target may be of type Form_pg_class or Form_pg_temp_class.
+ *
+ * Beware of multiple evaluations of arguments!
+ */
+#define COPY_PG_TEMP_CLASS_ATTRS(source, target) \
+	do { \
+		(target)->oid = (source)->oid; \
+		(target)->relfilenode = (source)->relfilenode; \
+		(target)->reltablespace = (source)->reltablespace; \
+	} while (0)
+
+/*
+ * Get the effective value of relfilenode from pg_class and pg_temp_class
+ * tuple data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline Oid
+GetEffective_relfilenode(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->relfilenode : cf->relfilenode;
+}
+
+/*
+ * Get the effective value of reltablespace from pg_class and pg_temp_class
+ * tuple data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline Oid
+GetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->reltablespace : cf->reltablespace;
+}
+
+/*
+ * Set the effective value of relfilenode in tuple form data from pg_class or
+ * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.
+ */
+static inline void
+SetEffective_relfilenode(Form_pg_class cf, Form_pg_temp_class tf, Oid val)
+{
+	if (tf != NULL)
+		tf->relfilenode = val;
+	else
+		cf->relfilenode = val;
+}
+
+/*
+ * Set the effective value of reltablespace in tuple form data from pg_class
+ * and pg_temp_class.  The value is set in pg_temp_class as well as pg_class,
+ * if the pg_temp_class tuple form data is non-NULL.
+ */
+static inline void
+SetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf, Oid val)
+{
+	/* NB: Value is set *both* locally and globally */
+	cf->reltablespace = val;
+	if (tf != NULL)
+		tf->reltablespace = val;
+}
+
+
+extern HeapTuple GetPgTempClassTuple(Oid relid);
+
+extern void InsertPgTempClassTuple(Relation rel);
+
+extern void UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple);
+
+extern void DeletePgTempClassTuple(Oid relid);
+
+extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
+												HeapTuple *temp_tuple,
+												bool check_temp);
+
+extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+
+extern void PreCCI_PgTempClass(void);
+
+extern void PreCommit_PgTempClass(void);
+
+extern void PreSubCommit_PgTempClass(void);
+
+extern void AtEOXact_PgTempClass(bool isCommit);
+
+extern void AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
+									SubTransactionId parentSubid);
+
+#endif							/* PG_TEMP_CLASS_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 748188f5393..a891c5afc01 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -1,5 +1,16 @@
 Parsed test spec with 2 sessions
 
+starting permutation: create_tblspace list_tblspaces
+step create_tblspace: CREATE TABLESPACE isolation_tablespace LOCATION '';
+step list_tblspaces: SELECT spcname FROM pg_tablespace ORDER BY 1;
+spcname             
+--------------------
+isolation_tablespace
+pg_default          
+pg_global           
+(3 rows)
+
+
 starting permutation: ins1 ins2 sel1 sel2
 step ins1: INSERT INTO tmp VALUES (1, 's1');
 step ins2: INSERT INTO tmp VALUES (1, 's2');
@@ -384,3 +395,101 @@ key|val|seq
   1|s2 |  1
 (1 row)
 
+
+starting permutation: ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step t2: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+  1|s1 |  1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+(0 rows)
+
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step t1: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+(0 rows)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+  1|s2 |  2
+(1 row)
+
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step t2: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+  1|s1 |  2
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+(0 rows)
+
+
+starting permutation: ins1 ins2 alt_tblspace get_tblspace1 get_tblspace2 sel1 sel2 reset_tblspace
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step alt_tblspace: ALTER TABLE tmp SET TABLESPACE isolation_tablespace;
+step get_tblspace1: 
+  SELECT s1.spcname, s2.spcname,
+         regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+    FROM pg_class c
+    JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+    LEFT JOIN pg_temp_class t ON t.oid = c.oid
+    JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+   WHERE c.relname = 'tmp';
+
+spcname             |spcname             |regexp_replace                       
+--------------------+--------------------+-------------------------------------
+isolation_tablespace|isolation_tablespace|pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+(1 row)
+
+step get_tblspace2: 
+  SELECT s1.spcname, s2.spcname,
+         regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+    FROM pg_class c
+    JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+    LEFT JOIN pg_temp_class t ON t.oid = c.oid
+    LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+   WHERE c.relname = 'tmp';
+
+spcname             |spcname|regexp_replace   
+--------------------+-------+-----------------
+isolation_tablespace|       |base/NNN/tNNN_NNN
+(1 row)
+
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+  1|s1 |  1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+  1|s2 |  1
+(1 row)
+
+step reset_tblspace: ALTER TABLE tmp SET TABLESPACE pg_default;
+
+starting permutation: drop_tblspace list_tblspaces
+step drop_tblspace: DROP TABLESPACE isolation_tablespace;
+step list_tblspaces: SELECT spcname FROM pg_tablespace ORDER BY 1;
+spcname   
+----------
+pg_default
+pg_global 
+(2 rows)
+
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index 0099ef86a1a..900643fda68 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -13,6 +13,10 @@ teardown {
 }
 
 session s1
+setup { SET allow_in_place_tablespaces = true; }
+step create_tblspace { CREATE TABLESPACE isolation_tablespace LOCATION ''; }
+step list_tblspaces { SELECT spcname FROM pg_tablespace ORDER BY 1; }
+step drop_tblspace { DROP TABLESPACE isolation_tablespace; }
 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'); }
@@ -32,6 +36,18 @@ step sel1_idx {
   SELECT * FROM tmp WHERE val = 's1';
   SELECT * FROM tmp WHERE val = 's1';
 }
+step t1 { TRUNCATE tmp; }
+step alt_tblspace { ALTER TABLE tmp SET TABLESPACE isolation_tablespace; }
+step get_tblspace1 {
+  SELECT s1.spcname, s2.spcname,
+         regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+    FROM pg_class c
+    JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+    LEFT JOIN pg_temp_class t ON t.oid = c.oid
+    JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+   WHERE c.relname = 'tmp';
+}
+step reset_tblspace { ALTER TABLE tmp SET TABLESPACE pg_default; }
 
 session s2
 step b2 { BEGIN; }
@@ -55,6 +71,18 @@ step sel2_idx {
   SELECT * FROM tmp WHERE val = 's2';
 }
 step reidx2 { REINDEX INDEX tmp_val_idx; }
+step get_tblspace2 {
+  SELECT s1.spcname, s2.spcname,
+         regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+    FROM pg_class c
+    JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+    LEFT JOIN pg_temp_class t ON t.oid = c.oid
+    LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+   WHERE c.relname = 'tmp';
+}
+
+# Create test tablespace for remaining tests
+permutation create_tblspace list_tblspaces
 
 # Basic effects
 permutation ins1 ins2 sel1 sel2
@@ -75,3 +103,12 @@ permutation create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
 permutation ins1 idx1 sel1_idx ins2 sel2_idx
 permutation ins1 ins2 idx1 sel1_idx sel2_idx
 permutation ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx
+
+# Test local TRUNCATE
+permutation ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
+
+# Test ALTER TABLE ... SET TABLESPACE
+permutation ins1 ins2 alt_tblspace get_tblspace1 get_tblspace2 sel1 sel2 reset_tblspace
+
+# Tidy up
+permutation drop_tblspace list_tblspaces
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index 8f25b5dd165..8fd95980f36 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -29,6 +29,7 @@ sub check_orphan_relfilenodes
 		'postgres', "
 	   SELECT pg_relation_filepath(oid) FROM pg_class
 	   WHERE reltablespace = 0 AND relpersistence <> 't' AND
+       relpersistence <> 'g' AND
 	   pg_relation_filepath(oid) IS NOT NULL;");
 	is_deeply(
 		[
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index d349f2b9ae3..9bf6e50c464 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -67,6 +67,14 @@ SELECT * FROM tmp1;
 ---+---+---
 (0 rows)
 
+-- Test pg_relation_filenode() matches global relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+  FROM pg_class WHERE oid = 'tmp1'::regclass;
+ ok 
+----
+ t
+(1 row)
+
 -- Test index
 INSERT INTO tmp1 VALUES (1, 'xxx');
 SET enable_seqscan = off;
@@ -105,7 +113,26 @@ SELECT * FROM tmp1 WHERE b = 'xxx';
 RESET enable_seqscan;
 REINDEX INDEX CONCURRENTLY tmp1_b_idx;
 REINDEX TABLE CONCURRENTLY tmp1;
+-- Test REINDEX -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx' \gset
+REINDEX INDEX tmp1_b_idx;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx';
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
 DROP INDEX CONCURRENTLY tmp1_b_idx;
+-- REINDEX not allowed on pg_temp_class
+REINDEX INDEX pg_temp_class_oid_index;
+NOTICE:  cannot reindex temporary system index "pg_temp_class_oid_index", skipping
+REINDEX TABLE pg_temp_class;
+NOTICE:  cannot reindex temporary system index "pg_temp_class_oid_index", skipping
 -- Test ON COMMIT DELETE ROWS
 CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
 BEGIN;
@@ -201,7 +228,7 @@ SELECT * FROM tmp2;
 (0 rows)
 
 DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
--- Test ALTER TABLE ... SET TABLESPACE
+-- Test ALTER TABLE ... SET TABLESPACE -- reltablespace changes locally and globally
 CREATE GLOBAL TEMP TABLE tmp2 (a int);
 INSERT INTO tmp2 VALUES (1);
 SELECT * FROM tmp2;
@@ -210,10 +237,15 @@ SELECT * FROM tmp2;
  1
 (1 row)
 
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
-  regexp_replace   
--------------------
- base/NNN/tNNN_NNN
+SELECT c.reltablespace AS global_tablespace,
+       t.reltablespace AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c
+  LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp2';
+ global_tablespace | local_tablespace |  regexp_replace   
+-------------------+------------------+-------------------
+                 0 |                0 | base/NNN/tNNN_NNN
 (1 row)
 
 ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
@@ -223,10 +255,16 @@ SELECT * FROM tmp2;
  1
 (1 row)
 
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
-            regexp_replace             
----------------------------------------
- pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+SELECT s1.spcname AS global_tablespace, s2.spcname AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c
+  JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+  LEFT JOIN pg_temp_class t ON t.oid = c.oid
+  JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp2';
+ global_tablespace | local_tablespace |            regexp_replace             
+-------------------+------------------+---------------------------------------
+ regress_tblspace  | regress_tblspace | pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
 (1 row)
 
 DROP TABLE tmp2;
@@ -317,6 +355,64 @@ SELECT * FROM tmp1;
 ---+---+---
 (0 rows)
 
+-- Test CLUSTER -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+CLUSTER tmp1 USING tmp1_pkey;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
+-- CLUSTER not allowed on pg_temp_class
+CLUSTER pg_temp_class; -- fail
+ERROR:  cannot execute CLUSTER on temporary system catalog "pg_temp_class"
+-- Test REPACK -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+REPACK tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
+-- REPACK not allowed on pg_temp_class
+REPACK pg_temp_class; -- fail
+ERROR:  cannot execute REPACK on temporary system catalog "pg_temp_class"
+-- Test VACUUM FULL -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+VACUUM FULL tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
+-- VACUUM FULL not allowed on pg_temp_class
+VACUUM FULL pg_temp_class; -- silently ignored
+-- Test pg_relation_filenode() now matches local relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+  FROM pg_temp_class WHERE oid = 'tmp1'::regclass;
+ ok 
+----
+ t
+(1 row)
+
 -- Test view creation
 INSERT INTO tmp1 VALUES (1, 'xxx');
 CREATE VIEW v AS SELECT * FROM tmp1;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index d64169b7bf0..3c3404e51b8 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -285,3 +285,5 @@ NOTICE:  checking pg_propgraph_label_property {plpellabelid} => pg_propgraph_ele
 NOTICE:  checking pg_propgraph_property {pgppgid} => pg_class {oid}
 NOTICE:  checking pg_propgraph_property {pgptypid} => pg_type {oid}
 NOTICE:  checking pg_propgraph_property {pgpcollation} => pg_collation {oid}
+NOTICE:  checking pg_temp_class {oid} => pg_class {oid}
+NOTICE:  checking pg_temp_class {reltablespace} => pg_tablespace {oid}
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index bbb1db3c433..c3e20ea8d07 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -88,6 +88,7 @@ standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
 standalone backend|relation|vacuum
+standalone backend|temp relation|normal
 standalone backend|wal|init
 standalone backend|wal|normal
 startup|relation|bulkread
@@ -111,7 +112,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(95 rows)
+(96 rows)
 \a
 -- ensure that both seqscan and indexscan plans are allowed
 SET enable_seqscan TO on;
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 9dee8279c42..97ebef118af 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -35,6 +35,10 @@ SELECT * FROM tmp1;
 SET search_path = global_temp_tests;
 SELECT * FROM tmp1;
 
+-- Test pg_relation_filenode() matches global relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+  FROM pg_class WHERE oid = 'tmp1'::regclass;
+
 -- Test index
 INSERT INTO tmp1 VALUES (1, 'xxx');
 SET enable_seqscan = off;
@@ -52,8 +56,22 @@ SELECT * FROM tmp1 WHERE b = 'xxx';
 RESET enable_seqscan;
 REINDEX INDEX CONCURRENTLY tmp1_b_idx;
 REINDEX TABLE CONCURRENTLY tmp1;
+
+-- Test REINDEX -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx' \gset
+REINDEX INDEX tmp1_b_idx;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx';
 DROP INDEX CONCURRENTLY tmp1_b_idx;
 
+-- REINDEX not allowed on pg_temp_class
+REINDEX INDEX pg_temp_class_oid_index;
+REINDEX TABLE pg_temp_class;
+
 -- Test ON COMMIT DELETE ROWS
 CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
 BEGIN;
@@ -110,14 +128,25 @@ DELETE FROM gtemp_pk_rel WHERE a = 1;
 SELECT * FROM tmp2;
 DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
 
--- Test ALTER TABLE ... SET TABLESPACE
+-- Test ALTER TABLE ... SET TABLESPACE -- reltablespace changes locally and globally
 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');
+SELECT c.reltablespace AS global_tablespace,
+       t.reltablespace AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c
+  LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp2';
 ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
 SELECT * FROM tmp2;
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+SELECT s1.spcname AS global_tablespace, s2.spcname AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c
+  JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+  LEFT JOIN pg_temp_class t ON t.oid = c.oid
+  JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp2';
 DROP TABLE tmp2;
 
 -- Test dependency on tablespace
@@ -169,6 +198,49 @@ SELECT * FROM tmp1;
 TRUNCATE tmp1;
 SELECT * FROM tmp1;
 
+-- Test CLUSTER -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+CLUSTER tmp1 USING tmp1_pkey;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+
+-- CLUSTER not allowed on pg_temp_class
+CLUSTER pg_temp_class; -- fail
+
+-- Test REPACK -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+REPACK tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+
+-- REPACK not allowed on pg_temp_class
+REPACK pg_temp_class; -- fail
+
+-- Test VACUUM FULL -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+VACUUM FULL tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+       CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+  FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+
+-- VACUUM FULL not allowed on pg_temp_class
+VACUUM FULL pg_temp_class; -- silently ignored
+
+-- Test pg_relation_filenode() now matches local relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+  FROM pg_temp_class WHERE oid = 'tmp1'::regclass;
+
 -- Test view creation
 INSERT INTO tmp1 VALUES (1, 'xxx');
 CREATE VIEW v AS SELECT * FROM tmp1;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4b7798b0f9d..e56a1b5e505 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -953,6 +953,7 @@ FormData_pg_statistic_ext_data
 FormData_pg_subscription
 FormData_pg_subscription_rel
 FormData_pg_tablespace
+FormData_pg_temp_class
 FormData_pg_transform
 FormData_pg_trigger
 FormData_pg_ts_config
@@ -1018,6 +1019,7 @@ Form_pg_statistic_ext_data
 Form_pg_subscription
 Form_pg_subscription_rel
 Form_pg_tablespace
+Form_pg_temp_class
 Form_pg_transform
 Form_pg_trigger
 Form_pg_ts_config
@@ -2253,6 +2255,7 @@ PatternInfoArray
 Pattern_Prefix_Status
 Pattern_Type
 PendingFsyncEntry
+PendingInsert
 PendingListenAction
 PendingListenEntry
 PendingRelDelete
-- 
2.51.0

