From 121048ebe85349f273a6f20da043596dad1f7287 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Fri, 13 Mar 2026 10:22:37 -0400
Subject: [PATCH 01/12] Global temporary tables: catalog and DDL support

Add a new relpersistence value RELPERSISTENCE_GLOBAL_TEMP ('g') for
global temporary tables.  Per the SQL standard, global temporary tables
have a persistent, shared catalog definition visible to all sessions,
but per-session private data (the per-session data isolation is not yet
implemented and will come in a subsequent commit).

The grammar now produces RELPERSISTENCE_GLOBAL_TEMP for CREATE GLOBAL
TEMPORARY TABLE, replacing the previous deprecation warning.  ON COMMIT
PRESERVE ROWS and DELETE ROWS are accepted; ON COMMIT DROP is rejected
since the table definition is persistent.

All relpersistence switch statements across the backend are updated to
handle the new value: catalog.c, storage.c, relcache.c, bufmgr.c,
dbsize.c, and tablecmds.c.  The new persistence type uses shared
buffers and no WAL, similar to unlogged tables; a subsequent commit
redirects it to local buffers.

Sequences implicitly created for SERIAL or IDENTITY columns of a GTT
inherit the GTT persistence, so each session gets its own counter.
parse_utilcmd propagates RELPERSISTENCE_GLOBAL_TEMP to the generated
CreateSeqStmt, and relation_open seeds the per-session sequence page
on first open from pg_sequence's initial state, so even a direct scan
of the sequence relation (SELECT last_value FROM seq, as psql's \d
emits) sees the one mandatory row in any session.  Standalone CREATE
GLOBAL TEMPORARY SEQUENCE is also supported.

GetNewRelFileNumber probes the backend's temp-relation namespace for
RELPERSISTENCE_GLOBAL_TEMP, since that is where a GTT's per-session
files actually live; the shared path never holds a file for a GTT.

psql \dt+ shows "global temporary" in the Persistence column.
pg_dump emits CREATE GLOBAL TEMPORARY TABLE.

Restrictions enforced:
- FK constraints between GTTs and non-GTTs are rejected
- ALTER TABLE SET LOGGED/UNLOGGED is rejected for GTTs
- ALTER TABLE SET TABLESPACE is rejected for GTTs (would rotate the
  shared-catalog relfilenode that every session uses as the stem of
  its own per-session file name)
- Mixing GTTs and local temp tables in inheritance is rejected, both
  at CREATE TABLE time (MergeAttributes) and at ALTER TABLE INHERIT /
  ATTACH PARTITION time
- GTTs cannot be created in temporary schemas
- CREATE GLOBAL TEMPORARY VIEW and CREATE GLOBAL TEMPORARY PROPERTY
  GRAPH are rejected: such relations have no storage for the GTT
  machinery to manage

- CREATE MATERIALIZED VIEW over a GTT is rejected: the matview's heap is
  permanent and shared, so populating it would capture one session's
  private rows.  Direct references are caught at creation time via
  query_uses_temp_object (which gains a global_temp_matters flag, false
  for the view-downgrade and function-lifetime callers); references
  through a view are caught at population time, where the planner has
  flattened the view into the range table -- covering both CREATE ...
  WITH DATA and REFRESH.  Plain views over GTTs remain allowed and stay
  permanent: they re-evaluate per session.
---
 src/backend/access/common/relation.c |  24 +++++
 src/backend/catalog/catalog.c        |  13 +++
 src/backend/catalog/dependency.c     |  28 +++++-
 src/backend/catalog/namespace.c      |  15 +++
 src/backend/catalog/pg_proc.c        |   3 +-
 src/backend/catalog/storage.c        |   1 +
 src/backend/commands/matview.c       |  21 ++++
 src/backend/commands/propgraphcmds.c |   6 ++
 src/backend/commands/sequence.c      |  56 ++++++++++-
 src/backend/commands/tablecmds.c     | 142 +++++++++++++++++++++++++--
 src/backend/commands/view.c          |  12 ++-
 src/backend/parser/analyze.c         |  27 +++--
 src/backend/parser/gram.y            |  36 ++-----
 src/backend/storage/buffer/bufmgr.c  |   3 +-
 src/backend/utils/adt/dbsize.c       |   1 +
 src/backend/utils/cache/relcache.c   |   2 +
 src/bin/pg_dump/pg_dump.c            |   2 +
 src/bin/psql/describe.c              |   2 +
 src/include/catalog/dependency.h     |   4 +-
 src/include/catalog/pg_class.h       |   1 +
 src/include/commands/sequence.h      |   2 +
 src/include/utils/rel.h              |  11 +++
 22 files changed, 361 insertions(+), 51 deletions(-)

diff --git a/src/backend/access/common/relation.c b/src/backend/access/common/relation.c
index 38b356b8239..57eca0ee635 100644
--- a/src/backend/access/common/relation.c
+++ b/src/backend/access/common/relation.c
@@ -23,12 +23,30 @@
 #include "access/relation.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "commands/sequence.h"
 #include "pgstat.h"
 #include "storage/lmgr.h"
 #include "storage/lock.h"
 #include "utils/inval.h"
 #include "utils/syscache.h"
 
+static void relation_open_gtt_prepare(Relation r);
+
+/*
+ * relation_open_gtt_prepare
+ *		Lazily materialize the session-local pieces of a global temporary
+ *		relation that need more than bare storage: sequences must be seeded
+ *		with their initial tuple.  Doing this here, at the single chokepoint
+ *		every open funnels through, covers direct relation_open callers
+ *		(executor scans of a sequence) as well as the sequence functions.
+ */
+static void
+relation_open_gtt_prepare(Relation r)
+{
+	if (r->rd_rel->relkind == RELKIND_SEQUENCE)
+		GttEnsureSequenceInitialized(r);
+}
+
 
 /* ----------------
  *		relation_open - open any relation by relation OID
@@ -73,6 +91,9 @@ relation_open(Oid relationId, LOCKMODE lockmode)
 	if (RelationUsesLocalBuffers(r))
 		MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE;
 
+	if (RelationIsGlobalTemp(r))
+		relation_open_gtt_prepare(r);
+
 	pgstat_init_relation(r);
 
 	return r;
@@ -123,6 +144,9 @@ try_relation_open(Oid relationId, LOCKMODE lockmode)
 	if (RelationUsesLocalBuffers(r))
 		MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE;
 
+	if (RelationIsGlobalTemp(r))
+		relation_open_gtt_prepare(r);
+
 	pgstat_init_relation(r);
 
 	return r;
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 7be49032934..48fb623b2e8 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -573,6 +573,19 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence)
 		case RELPERSISTENCE_TEMP:
 			procNumber = ProcNumberForTempRelations();
 			break;
+
+			/*
+			 * A global temporary table never has a file at the shared path;
+			 * its per-session files live in each backend's temp-relation
+			 * namespace, using this relfilenumber.  Probe our own temp
+			 * namespace, the same one the file will be created in for this
+			 * session; other backends' namespaces cannot be checked here,
+			 * which is the same (wraparound-only) exposure regular temp
+			 * tables have.
+			 */
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			procNumber = ProcNumberForTempRelations();
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			procNumber = INVALID_PROC_NUMBER;
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index c54774b3275..fd4cc08c724 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
@@ -2533,6 +2534,14 @@ process_function_rte_ref(RangeTblEntry *rte, AttrNumber attnum,
  * local_temp_okay is true.  If one is found, return true after storing its
  * address in *foundobj.
  *
+ * If include_gtt is true, relations that are global temporary
+ * tables also count as temporary objects.  GTTs live in ordinary schemas,
+ * so the namespace test never catches them; but their *data* is
+ * session-private, which is what callers like the materialized-view check
+ * care about.  Callers that only care about object lifetime (e.g. the
+ * temporary-view downgrade, where the GTT definition's persistence is what
+ * matters) pass false.
+ *
  * Current callers only use this to deliver helpful notices, so reporting
  * one such object seems sufficient.  We return the first one, which should
  * be a stable result for a given query since it depends only on the order
@@ -2542,13 +2551,22 @@ process_function_rte_ref(RangeTblEntry *rte, AttrNumber attnum,
  */
 bool
 find_temp_object(const ObjectAddresses *addrs, bool local_temp_okay,
-				 ObjectAddress *foundobj)
+				 bool include_gtt, ObjectAddress *foundobj)
 {
 	for (int i = 0; i < addrs->numrefs; i++)
 	{
 		const ObjectAddress *thisobj = addrs->refs + i;
 		Oid			objnamespace;
 
+		/* Global temporary tables hold session-private data. */
+		if (include_gtt &&
+			thisobj->classId == RelationRelationId &&
+			get_rel_persistence(thisobj->objectId) == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			*foundobj = *thisobj;
+			return true;
+		}
+
 		/*
 		 * Use get_object_namespace() to see if this object belongs to a
 		 * schema.  If not, we can skip it.
@@ -2573,10 +2591,12 @@ find_temp_object(const ObjectAddresses *addrs, bool local_temp_okay,
  * query_uses_temp_object - convenience wrapper for find_temp_object
  *
  * If the Query includes any use of a temporary object, fill *temp_object
- * with the address of one such object and return true.
+ * with the address of one such object and return true.  See
+ * find_temp_object for the meaning of include_gtt.
  */
 bool
-query_uses_temp_object(Query *query, ObjectAddress *temp_object)
+query_uses_temp_object(Query *query, bool include_gtt,
+					   ObjectAddress *temp_object)
 {
 	bool		result;
 	ObjectAddresses *addrs;
@@ -2587,7 +2607,7 @@ query_uses_temp_object(Query *query, ObjectAddress *temp_object)
 	collectDependenciesOfExpr(addrs, (Node *) query, NIL);
 
 	/* Look for one that is temp */
-	result = find_temp_object(addrs, false, temp_object);
+	result = find_temp_object(addrs, false, include_gtt, temp_object);
 
 	free_object_addresses(addrs);
 
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 56b87d878e8..e26651099f9 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -869,6 +869,21 @@ RangeVarAdjustRelationPersistence(RangeVar *newRelation, Oid nspid)
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 						 errmsg("cannot create relations in temporary schemas of other sessions")));
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+
+			/*
+			 * A global temporary table is a permanent catalog object that
+			 * merely carries per-session data, so it has no business living
+			 * in a temporary schema.  Reject it explicitly rather than
+			 * letting it fall through to the generic message below, which
+			 * would wrongly imply a global temporary table is not a temporary
+			 * relation.
+			 */
+			if (isAnyTempNamespace(nspid))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot create a global temporary table in a temporary schema")));
+			break;
 		default:
 			if (isAnyTempNamespace(nspid))
 				ereport(ERROR,
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 5df4b3f7a91..fa4b140bef6 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -676,7 +676,8 @@ ProcedureCreate(const char *procedureName,
 	 * function created in our own pg_temp namespace refers to other objects
 	 * in that namespace, since then they'll have similar lifespans anyway.
 	 */
-	if (find_temp_object(addrs, isTempNamespace(procNamespace), &temp_object))
+	if (find_temp_object(addrs, isTempNamespace(procNamespace), false,
+						 &temp_object))
 		ereport(NOTICE,
 				(errmsg("function \"%s\" will be effectively temporary",
 						procedureName),
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index e443a4993c5..febd8c246ca 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -135,6 +135,7 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
 			needs_wal = false;
 			break;
 		case RELPERSISTENCE_UNLOGGED:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			procNumber = INVALID_PROC_NUMBER;
 			needs_wal = false;
 			break;
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..36ebc95ea60 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -427,6 +427,27 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	/* Plan the query which will generate data for the refresh. */
 	plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL);
 
+	/*
+	 * A materialized view must not read a global temporary table: GTT
+	 * contents are session-private, while the matview's heap is permanent and
+	 * shared, so populating it would capture -- and publish -- one session's
+	 * private rows.  Direct references are rejected at creation time (see
+	 * transformCreateTableAsStmt), but a GTT reached through a view only
+	 * appears once the planner has flattened the view into the range table,
+	 * so check the planned rtable here.  This covers both CREATE MATERIALIZED
+	 * VIEW ... WITH DATA and REFRESH.
+	 */
+	foreach_node(RangeTblEntry, rte, plan->rtable)
+	{
+		if (rte->rtekind == RTE_RELATION &&
+			get_rel_persistence(rte->relid) == RELPERSISTENCE_GLOBAL_TEMP)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("materialized views must not use temporary objects"),
+					 errdetail("This view depends on global temporary table \"%s\".",
+							   get_rel_name(rte->relid))));
+	}
+
 	/*
 	 * Use a snapshot with an updated command ID to ensure this query sees
 	 * results of any previously executed queries.  (This could only matter if
diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c
index cc516e27020..28ed1338f60 100644
--- a/src/backend/commands/propgraphcmds.c
+++ b/src/backend/commands/propgraphcmds.c
@@ -117,6 +117,12 @@ CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt)
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("property graphs cannot be unlogged because they do not have storage")));
 
+	/* same for global temporary, see DefineView */
+	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/sequence.c b/src/backend/commands/sequence.c
index 551667650ba..8eef726a228 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -420,6 +420,56 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum)
 	UnlockReleaseBuffer(buf);
 }
 
+/*
+ * GttEnsureSequenceInitialized
+ *		Seed per-session storage for a global temporary sequence.
+ *
+ * A GTT sequence's catalog row is shared across sessions, but each session
+ * has its own physical storage (allocated lazily by GttInitSessionStorage).
+ * That storage starts out empty — block 0 does not exist — so any attempt
+ * to read the sequence tuple would fail.  Called from relation_open for
+ * every GTT sequence, so even a direct heapscan of the sequence (SELECT
+ * last_value FROM seq, as psql's \d emits) sees the one mandatory row.
+ * The first open in each session writes block 0, using the definition
+ * recorded in pg_sequence as the initial state (last_value = seqstart,
+ * log_cnt = 0, is_called = false).
+ */
+void
+GttEnsureSequenceInitialized(Relation rel)
+{
+	HeapTuple	pgstuple;
+	Form_pg_sequence pgsform;
+	Datum		value[SEQ_COL_LASTCOL];
+	bool		null[SEQ_COL_LASTCOL] = {0};
+	HeapTuple	tuple;
+
+	if (rel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
+		return;
+
+	if (RelationGetNumberOfBlocks(rel) > 0)
+		return;
+
+	/*
+	 * No pg_sequence row yet: we are inside CREATE SEQUENCE, opened from
+	 * DefineSequence before the row is inserted.  The creator fills the
+	 * initial tuple itself via fill_seq_with_data.
+	 */
+	pgstuple = SearchSysCache1(SEQRELID, ObjectIdGetDatum(RelationGetRelid(rel)));
+	if (!HeapTupleIsValid(pgstuple))
+		return;
+	pgsform = (Form_pg_sequence) GETSTRUCT(pgstuple);
+
+	value[SEQ_COL_LASTVAL - 1] = Int64GetDatumFast(pgsform->seqstart);
+	value[SEQ_COL_LOG - 1] = Int64GetDatum((int64) 0);
+	value[SEQ_COL_CALLED - 1] = BoolGetDatum(false);
+
+	ReleaseSysCache(pgstuple);
+
+	tuple = heap_form_tuple(RelationGetDescr(rel), value, null);
+	fill_seq_fork_with_data(rel, tuple, MAIN_FORKNUM);
+	heap_freetuple(tuple);
+}
+
 /*
  * AlterSequence
  *
@@ -1815,12 +1865,14 @@ pg_get_sequence_data(PG_FUNCTION_ARGS)
 
 	/*
 	 * Return all NULLs for missing sequences, sequences for which we lack
-	 * privileges, other sessions' temporary sequences, and unlogged sequences
-	 * on standbys.
+	 * privileges, other sessions' temporary sequences, global temporary
+	 * sequences (their data is per-session and not meaningful across backends
+	 * or in a dump), and unlogged sequences on standbys.
 	 */
 	if (seqrel && seqrel->rd_rel->relkind == RELKIND_SEQUENCE &&
 		pg_class_aclcheck(relid, GetUserId(), ACL_SELECT) == ACLCHECK_OK &&
 		!RELATION_IS_OTHER_TEMP(seqrel) &&
+		seqrel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
 		(RelationIsPermanent(seqrel) || !RecoveryInProgress()))
 	{
 		Buffer		buf;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 265dcfe7fda..82700c49ba0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -843,11 +843,18 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * Check consistency of arguments
 	 */
 	if (stmt->oncommit != ONCOMMIT_NOOP
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& 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 is not supported for global temporary tables"));
+
 	if (stmt->partspec != NULL)
 	{
 		if (relkind != RELKIND_RELATION)
@@ -1095,6 +1102,23 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			accessMethodId = get_table_am_oid(default_table_access_method, false);
 	}
 
+	/*
+	 * Global temporary tables rely on the heap table access method.  Their
+	 * per-session storage, local buffering, and tuple visibility handling are
+	 * all heap-specific (see storage_gtt.c), and the wraparound-safety
+	 * reasoning for GTTs assumes heap.  Reject any other access method --
+	 * whether requested with USING or inherited from
+	 * default_table_access_method -- rather than create a table that cannot
+	 * work correctly.
+	 */
+	if (stmt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+		OidIsValid(accessMethodId) &&
+		accessMethodId != HEAP_TABLE_AM_OID)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("access method \"%s\" is not supported for global temporary tables",
+					   get_am_name(accessMethodId)));
+
 	/*
 	 * Create the relation.  Inherited defaults and CHECK constraints are
 	 * passed in for immediate handling --- since they don't need parsing,
@@ -2762,7 +2786,9 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 		 */
 		if (is_partition &&
 			relation->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
-			relpersistence == RELPERSISTENCE_TEMP)
+			relation->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
+			(relpersistence == RELPERSISTENCE_TEMP ||
+			 relpersistence == RELPERSISTENCE_GLOBAL_TEMP))
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
@@ -2770,7 +2796,9 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 
 		/* Permanent rels cannot inherit from temporary ones */
 		if (relpersistence != RELPERSISTENCE_TEMP &&
-			relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
+			(relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+			 RelationIsGlobalTemp(relation)))
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg(!is_partition
@@ -2778,6 +2806,19 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 							: "cannot create a permanent relation as partition of temporary relation \"%s\"",
 							RelationGetRelationName(relation))));
 
+		/*
+		 * Don't allow mixing global temporary tables with local temporary
+		 * tables in inheritance or partitioning hierarchies, in either
+		 * direction.
+		 */
+		if ((relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+			 relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP) ||
+			(relpersistence == RELPERSISTENCE_TEMP &&
+			 RelationIsGlobalTemp(relation)))
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("cannot mix global temporary and local temporary tables in inheritance"));
+
 		/* If existing rel is temp, it must belong to this session */
 		if (RELATION_IS_OTHER_TEMP(relation))
 			ereport(ERROR,
@@ -6005,6 +6046,21 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("cannot rewrite temporary tables of other sessions")));
 
+			/*
+			 * A table rewrite rotates the catalog relfilenode.  For a GTT,
+			 * every session's per-session storage is keyed by the catalog
+			 * relfilenode, so rotating it would leave other sessions pointing
+			 * at files that no longer exist or at the wrong generation of the
+			 * table.  Block rewrites for GTTs along with the other
+			 * relfilenode-rotating commands (CLUSTER / REINDEX / SET
+			 * TABLESPACE / SET LOGGED|UNLOGGED).
+			 */
+			if (RelationIsGlobalTemp(OldHeap))
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("cannot rewrite global temporary table \"%s\"",
+							   RelationGetRelationName(OldHeap)));
+
 			/*
 			 * Select destination tablespace (same as original unless user
 			 * requested a change)
@@ -10235,6 +10291,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 						 errmsg("constraints on temporary tables must involve temporary tables of this session")));
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("constraints on global temporary tables may reference only global temporary tables"));
+			break;
 	}
 
 	/*
@@ -16873,6 +16935,18 @@ ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacen
 {
 	Oid			tablespaceId;
 
+	/*
+	 * SET TABLESPACE rewrites the file and assigns a new relfilenode.  For
+	 * GTTs the catalog relfilenode is the stem of every session's local file
+	 * name, so rotating it would desynchronize all other sessions'
+	 * per-session storage mappings.
+	 */
+	if (RelationIsGlobalTemp(rel))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot change tablespace of global temporary table \"%s\"",
+					   RelationGetRelationName(rel)));
+
 	/* Check that the tablespace exists */
 	tablespaceId = get_tablespace_oid(tablespacename, false);
 
@@ -17538,6 +17612,23 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
 				 errmsg("cannot inherit from temporary relation \"%s\"",
 						RelationGetRelationName(parent_rel))));
 
+	/*
+	 * GTTs mix neither with permanent nor with local temporary relations. See
+	 * MergeAttributes() for the CREATE TABLE side of this rule.
+	 */
+	if (RelationIsGlobalTemp(parent_rel) &&
+		child_rel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot inherit from global temporary relation \"%s\"",
+					   RelationGetRelationName(parent_rel)));
+	if (parent_rel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
+		RelationIsGlobalTemp(child_rel))
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot inherit into global temporary relation \"%s\"",
+					   RelationGetRelationName(child_rel)));
+
 	/* If parent rel is temp, it must belong to this session */
 	if (RELATION_IS_OTHER_TEMP(parent_rel))
 		ereport(ERROR,
@@ -19087,6 +19178,13 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 							RelationGetRelationName(rel)),
 					 errtable(rel)));
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change logged status of table \"%s\" because it is a global temporary table",
+						   RelationGetRelationName(rel)),
+					errtable(rel));
+			break;
 		case RELPERSISTENCE_PERMANENT:
 			if (toLogged)
 				/* nothing to do */
@@ -20686,20 +20784,38 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 
 	/* If the parent is permanent, so must be all of its partitions. */
 	if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
-		attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		rel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
+		(attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 RelationIsGlobalTemp(attachrel)))
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("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 */
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
-		attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+	if ((rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 RelationIsGlobalTemp(rel)) &&
+		attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		attachrel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
 						RelationGetRelationName(rel))));
 
+	/* GTT and local-temp cannot mix as partition parent/child */
+	if (RelationIsGlobalTemp(rel) &&
+		attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot attach a local temporary relation as partition of global temporary relation \"%s\"",
+					   RelationGetRelationName(rel)));
+	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+		RelationIsGlobalTemp(attachrel))
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot attach a global temporary relation as partition of local temporary relation \"%s\"",
+					   RelationGetRelationName(rel)));
+
 	/* If the parent is temp, it must belong to this session */
 	if (RELATION_IS_OTHER_TEMP(rel))
 		ereport(ERROR,
@@ -22786,6 +22902,20 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
 				errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
 					   RelationGetRelationName(parent_rel)));
 
+	/*
+	 * Splitting or merging partitions of a global temporary table is not
+	 * supported.  The new partition created here would not inherit the
+	 * parent's global temporary persistence, so it would be given permanent,
+	 * cluster-wide storage underneath a parent whose data is per-session.
+	 * Reject the command rather than silently creating such an inconsistent
+	 * partition (this function is only reached from SPLIT/MERGE PARTITION).
+	 */
+	if (parent_relform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot split or merge partitions of global temporary table \"%s\"",
+					   RelationGetRelationName(parent_rel)));
+
 	/* Create the relation. */
 	newRelId = heap_create_with_catalog(newPartName->relname,
 										namespaceId,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 1bd78a4cdf0..b124ca2b514 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -476,6 +476,16 @@ DefineView(ViewStmt *stmt, const char *queryString,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("views cannot be unlogged because they do not have storage")));
 
+	/*
+	 * Nor are global temporary views: a view has no per-session data for the
+	 * GTT machinery to manage, and downstream code would wrongly treat the
+	 * relation as having session-private storage.
+	 */
+	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
@@ -484,7 +494,7 @@ DefineView(ViewStmt *stmt, const char *queryString,
 	 */
 	view = copyObject(stmt->view);	/* don't corrupt original command */
 	if (view->relpersistence == RELPERSISTENCE_PERMANENT
-		&& query_uses_temp_object(viewParse, &temp_object))
+		&& query_uses_temp_object(viewParse, false, &temp_object))
 	{
 		view->relpersistence = RELPERSISTENCE_TEMP;
 		ereport(NOTICE,
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 93fa66ae57c..24cc06776d2 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -3536,14 +3536,27 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
 		/*
 		 * Check whether any temporary database objects are used in the
 		 * creation query. It would be hard to refresh data or incrementally
-		 * maintain it if a source disappeared.
+		 * maintain it if a source disappeared.  Global temporary tables
+		 * count: their definition persists, but their contents are
+		 * session-private, so materializing them into a permanent relation
+		 * would capture (and publish) one session's private rows.
 		 */
-		if (query_uses_temp_object(query, &temp_object))
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("materialized views must not use temporary objects"),
-					 errdetail("This view depends on temporary %s.",
-							   getObjectDescription(&temp_object, false))));
+		if (query_uses_temp_object(query, true, &temp_object))
+		{
+			if (temp_object.classId == RelationRelationId &&
+				get_rel_persistence(temp_object.objectId) == RELPERSISTENCE_GLOBAL_TEMP)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("materialized views must not use temporary objects"),
+						 errdetail("This view depends on global temporary table \"%s\".",
+								   get_rel_name(temp_object.objectId))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("materialized views must not use temporary objects"),
+						 errdetail("This view depends on temporary %s.",
+								   getObjectDescription(&temp_object, false))));
+		}
 
 		/*
 		 * A materialized view would either need to save parameters for use in
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..44e28b9d588 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3913,31 +3913,17 @@ 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
- * 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.
+ * NOTE: GLOBAL creates a SQL-standard global temporary table, visible to
+ * all sessions but with per-session data.  LOCAL is treated as equivalent
+ * to plain TEMPORARY (session-local), as most other products implement it;
+ * since we have no module-level scope, LOCAL remains a noise word.
  */
 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 +13958,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/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d6c0cc1f6d4..cd0bcdf2fe8 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1237,7 +1237,8 @@ PinBufferForBlock(Relation rel,
 	/* Persistence should be set before */
 	Assert((persistence == RELPERSISTENCE_TEMP ||
 			persistence == RELPERSISTENCE_PERMANENT ||
-			persistence == RELPERSISTENCE_UNLOGGED));
+			persistence == RELPERSISTENCE_UNLOGGED ||
+			persistence == RELPERSISTENCE_GLOBAL_TEMP));
 
 	TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
 									   smgr->smgr_rlocator.locator.spcOid,
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..7243ec0dfd9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1020,6 +1020,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 	{
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			backend = INVALID_PROC_NUMBER;
 			break;
 		case RELPERSISTENCE_TEMP:
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0572ab424e7..d0219396cc4 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1158,6 +1158,7 @@ retry:
 	{
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			relation->rd_backend = INVALID_PROC_NUMBER;
 			relation->rd_islocaltemp = false;
 			break;
@@ -3653,6 +3654,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			rel->rd_backend = INVALID_PROC_NUMBER;
 			rel->rd_islocaltemp = false;
 			break;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c56437d6057..0c42d81a0be 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -17453,6 +17453,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		 * ignore it when dumping if it was set in this case.
 		 */
 		appendPQExpBuffer(q, "CREATE %s%s %s",
+						  tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+						  "GLOBAL TEMPORARY " :
 						  (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED &&
 						   tbinfo->relkind != RELKIND_PARTITIONED_TABLE) ?
 						  "UNLOGGED " : "",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index af3935b0078..a33b6a0bcab 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4311,10 +4311,12 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 						  "WHEN " CppAsString2(RELPERSISTENCE_PERMANENT) " THEN '%s' "
 						  "WHEN " CppAsString2(RELPERSISTENCE_TEMP) " THEN '%s' "
 						  "WHEN " CppAsString2(RELPERSISTENCE_UNLOGGED) " THEN '%s' "
+						  "WHEN " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " THEN '%s' "
 						  "END as \"%s\"",
 						  gettext_noop("permanent"),
 						  gettext_noop("temporary"),
 						  gettext_noop("unlogged"),
+						  gettext_noop("global temporary"),
 						  gettext_noop("Persistence"));
 		translate_columns[cols_so_far] = true;
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 2f3c1eae3c7..98de33d32a9 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -127,9 +127,11 @@ extern void recordDependencyOnSingleRelExpr(const ObjectAddress *depender,
 
 extern bool find_temp_object(const ObjectAddresses *addrs,
 							 bool local_temp_okay,
+							 bool include_gtt,
 							 ObjectAddress *foundobj);
 
-extern bool query_uses_temp_object(Query *query, ObjectAddress *temp_object);
+extern bool query_uses_temp_object(Query *query, bool include_gtt,
+								   ObjectAddress *temp_object);
 
 extern ObjectAddresses *new_object_addresses(void);
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index c4af599dc90..1f7ea2bf00b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -183,6 +183,7 @@ 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_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/commands/sequence.h b/src/include/commands/sequence.h
index 2c3c4a3f074..97914a574bf 100644
--- a/src/include/commands/sequence.h
+++ b/src/include/commands/sequence.h
@@ -17,6 +17,7 @@
 #include "fmgr.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
+#include "utils/relcache.h"
 
 typedef struct FormData_pg_sequence_data
 {
@@ -49,5 +50,6 @@ extern void DeleteSequenceTuple(Oid relid);
 extern void ResetSequence(Oid seq_relid);
 extern void SetSequence(Oid relid, int64 next, bool iscalled);
 extern void ResetSequenceCaches(void);
+extern void GttEnsureSequenceInitialized(Relation rel);
 
 #endif							/* SEQUENCE_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fa07ebf8ff7..246cbfd49ba 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -641,9 +641,20 @@ RelationCloseSmgr(Relation relation)
 	  (relation->rd_createSubid == InvalidSubTransactionId &&			\
 	   relation->rd_firstRelfilelocatorSubid == InvalidSubTransactionId)))
 
+/*
+ * RelationIsGlobalTemp
+ *		True if relation is a global temporary table.
+ */
+#define RelationIsGlobalTemp(relation) \
+	((relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+
 /*
  * RelationUsesLocalBuffers
  *		True if relation's pages are stored in local buffers.
+ *
+ * Note: global temporary tables will use local buffers for per-session
+ * data storage once that infrastructure is implemented.  For now, their
+ * shared catalog storage uses shared buffers like permanent tables.
  */
 #define RelationUsesLocalBuffers(relation) \
 	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
-- 
2.43.0

