From d67c6d671cfb26198bebca52820d68986f7b27fb Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Fri, 13 Mar 2026 12:18:50 -0400
Subject: [PATCH 05/12] Global temporary tables: disable parallel query and
 autovacuum

GTTs use per-session local buffers that parallel workers cannot access,
for the same reason as regular temporary tables.  Disable parallel
query for GTT scans in set_rel_consider_parallel() and disable parallel
CREATE INDEX in plan_create_index_workers().

Also skip GTTs in autovacuum's table scanning loops, since autovacuum
runs in a separate backend and cannot access any session's local GTT
storage.

The planner-side exclusion only covers GTTs used as parallel
baserels; a function mislabeled PARALLEL SAFE can still reach a GTT
from inside a worker.  GttInitSessionStorage therefore errors out
cleanly in parallel workers (mirroring the temp-table check in
InitLocalBuffers), and in the parallel-mode leader when first-time
storage initialization would be required, instead of tripping
RelationCreateStorage's assertion or scribbling on the leader's temp
namespace from a worker.
---
 src/backend/catalog/storage_gtt.c     | 39 +++++++++++++++++++++++++++
 src/backend/optimizer/path/allpaths.c | 24 ++++++++++-------
 src/backend/optimizer/plan/planner.c  |  7 ++---
 src/backend/optimizer/util/clauses.c  | 20 ++++++++++++++
 src/backend/postmaster/autovacuum.c   | 13 +++++++--
 5 files changed, 89 insertions(+), 14 deletions(-)

diff --git a/src/backend/catalog/storage_gtt.c b/src/backend/catalog/storage_gtt.c
index 3aa1d20f156..bd4540b59b1 100644
--- a/src/backend/catalog/storage_gtt.c
+++ b/src/backend/catalog/storage_gtt.c
@@ -22,6 +22,7 @@
 #include "postgres.h"
 
 #include "access/amapi.h"
+#include "access/parallel.h"
 #include "access/relation.h"
 #include "access/table.h"
 #include "access/tableam.h"
@@ -184,6 +185,33 @@ GttInitSessionStorage(Relation relation)
 	bool		found;
 	Oid			relid = RelationGetRelid(relation);
 
+	/*
+	 * A parallel worker must never create or register per-session storage:
+	 * the storage map is backend-local (a worker cannot see the leader's
+	 * entries or dirty local buffers), so any worker-side materialization
+	 * would corrupt or duplicate the leader's session state.  But a worker
+	 * may legitimately need only the relation's catalog metadata -- e.g.
+	 * pg_get_expr() opens the relation to deparse a column default while
+	 * pg_dump runs under debug_parallel_query.  Point the relcache entry at
+	 * the session-local file in the leader's temp namespace, exactly as an
+	 * untouched GTT looks in the leader, but without recording anything in
+	 * the storage map.  No file is created here; if the leader never
+	 * materialized the relation, reads short-circuit to empty.  The planner
+	 * never makes a GTT a parallel baserel and DML is never parallelized, so
+	 * a worker never actually scans or writes one.
+	 */
+	if (IsParallelWorker())
+	{
+		if (OidIsValid(relation->rd_rel->reltablespace))
+			relation->rd_locator.spcOid = relation->rd_rel->reltablespace;
+		else
+			relation->rd_locator.spcOid = MyDatabaseTableSpace;
+		relation->rd_locator.dbOid = MyDatabaseId;
+		relation->rd_locator.relNumber = relation->rd_rel->relfilenode;
+		relation->rd_backend = ProcNumberForTempRelations();
+		return;
+	}
+
 	ensure_gtt_hash();
 
 	entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
@@ -339,6 +367,17 @@ GttEnsureSessionStorage(Relation relation)
 	if (entry->storage_created)
 		return;
 
+	/*
+	 * RelationCreateStorage cannot run in parallel mode (it couldn't update
+	 * pendingSyncHash), and a worker must never materialize state in the
+	 * leader's temp namespace; relcache builds in workers are already
+	 * rejected in GttInitSessionStorage.
+	 */
+	if (IsInParallelMode())
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+				errmsg("cannot initialize global temporary table storage during a parallel operation"));
+
 	RelationCreateStorage(entry->locator, RELPERSISTENCE_GLOBAL_TEMP, true);
 	entry->storage_created = true;
 	entry->storage_subid = GetCurrentSubTransactionId();
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index c134594a21a..c40b71c902d 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -664,16 +664,22 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 
 			/*
 			 * Currently, parallel workers can't access the leader's temporary
-			 * tables.  We could possibly relax this if we wrote all of its
-			 * local buffers at the start of the query and made no changes
-			 * thereafter (maybe we could allow hint bit changes), and if we
-			 * taught the workers to read them.  Writing a large number of
-			 * temporary buffers could be expensive, though, and we don't have
-			 * the rest of the necessary infrastructure right now anyway.  So
-			 * for now, bail out if we see a temporary table.
+			 * tables, nor a global temporary table's per-session data.  We
+			 * could possibly relax this if we wrote all of its local buffers
+			 * at the start of the query and made no changes thereafter (maybe
+			 * we could allow hint bit changes), and if we taught the workers
+			 * to read them.  Writing a large number of temporary buffers
+			 * could be expensive, though, and we don't have the rest of the
+			 * necessary infrastructure right now anyway.  So for now, bail
+			 * out if we see a temporary or global temporary table.
 			 */
-			if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
-				return;
+			{
+				char		relpersist = get_rel_persistence(rte->relid);
+
+				if (relpersist == RELPERSISTENCE_TEMP ||
+					relpersist == RELPERSISTENCE_GLOBAL_TEMP)
+					return;
+			}
 
 			/*
 			 * Table sampling can be pushed down to workers if the sample
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index f4689e7c9f8..964dc4a0fa0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -7347,11 +7347,12 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)
 	/*
 	 * Determine if it's safe to proceed.
 	 *
-	 * Currently, parallel workers can't access the leader's temporary tables.
-	 * Furthermore, any index predicate or index expressions must be parallel
-	 * safe.
+	 * Currently, parallel workers can't access the leader's temporary tables,
+	 * nor a global temporary table's per-session data.  Furthermore, any
+	 * index predicate or index expressions must be parallel safe.
 	 */
 	if (heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		RelationIsGlobalTemp(heap) ||
 		!is_parallel_safe(root, (Node *) RelationGetIndexExpressions(index)) ||
 		!is_parallel_safe(root, (Node *) RelationGetIndexPredicate(index)))
 	{
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 01997e22266..8d98d5f8b43 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -965,6 +965,7 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 	else if (IsA(node, Query))
 	{
 		Query	   *query = (Query *) node;
+		ListCell   *lc;
 
 		/* SELECT FOR UPDATE/SHARE must be treated as unsafe */
 		if (query->rowMarks != NULL)
@@ -973,6 +974,25 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 			return true;
 		}
 
+		/*
+		 * A global temporary table has per-session, backend-local storage
+		 * that parallel workers cannot see, and that storage may have to be
+		 * created on first access -- which is impossible in parallel mode.
+		 * Referencing one therefore makes the query parallel-unsafe, so that
+		 * parallel mode is never imposed on it (e.g. by debug_parallel_query).
+		 */
+		foreach(lc, query->rtable)
+		{
+			RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
+
+			if (rte->rtekind == RTE_RELATION &&
+				get_rel_persistence(rte->relid) == RELPERSISTENCE_GLOBAL_TEMP)
+			{
+				context->max_hazard = PROPARALLEL_UNSAFE;
+				return true;
+			}
+		}
+
 		/* Recurse into subselects */
 		return query_tree_walker(query,
 								 max_parallel_hazard_walker,
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index e9aaf24c1be..7452518c386 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2073,6 +2073,13 @@ do_autovacuum(void)
 			continue;
 		}
 
+		/*
+		 * Global temporary tables have per-session local storage that
+		 * autovacuum cannot access.  Skip them.
+		 */
+		if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			continue;
+
 		/* Fetch reloptions and the pgstat entry for this table */
 		relopts = extract_autovac_opts(tuple, pg_class_desc);
 
@@ -2147,9 +2154,11 @@ do_autovacuum(void)
 		AutoVacuumScores scores;
 
 		/*
-		 * We cannot safely process other backends' temp tables, so skip 'em.
+		 * We cannot safely process other backends' temp tables or GTTs (which
+		 * have per-session local storage), so skip them.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 			continue;
 
 		relid = classForm->oid;
-- 
2.43.0

