0002-Global-temporary-tables-per-session-data-isolation.patch
text/x-patch
Filename: 0002-Global-temporary-tables-per-session-data-isolation.patch
Type: text/x-patch
Part: 1
Message:
Re: Global temporary tables
Patch
Format: format-patch
Series: patch 0002
Subject: Global temporary tables: per-session data isolation
| File | + | − |
|---|---|---|
| src/backend/access/table/tableam.c | 6 | 0 |
| src/backend/access/transam/xloginsert.c | 3 | 3 |
| src/backend/catalog/heap.c | 18 | 0 |
| src/backend/catalog/Makefile | 1 | 0 |
| src/backend/catalog/meson.build | 1 | 0 |
| src/backend/catalog/storage.c | 4 | 1 |
| src/backend/catalog/storage_gtt.c | 529 | 0 |
| src/backend/commands/createas.c | 61 | 2 |
| src/backend/commands/dbcommands.c | 5 | 2 |
| src/backend/commands/sequence.c | 13 | 0 |
| src/backend/commands/tablecmds.c | 9 | 18 |
| src/backend/storage/buffer/bufmgr.c | 21 | 10 |
| src/backend/utils/adt/dbsize.c | 17 | 1 |
| src/backend/utils/cache/relcache.c | 35 | 2 |
| src/include/catalog/pg_class.h | 8 | 0 |
| src/include/catalog/storage_gtt.h | 23 | 0 |
| src/include/utils/rel.h | 2 | 5 |
| src/tools/pgindent/typedefs.list | 1 | 0 |
From 7c04a7b997b6ae185b05cb7987dfb976e990cee5 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Fri, 13 Mar 2026 10:34:17 -0400
Subject: [PATCH 02/12] Global temporary tables: per-session data isolation
Each session that accesses a global temporary table now gets its own
private data storage, stored in local buffers. Session A's INSERT
into a GTT is invisible to session B, matching the SQL standard
semantics for global temporary tables.
The implementation uses a backend-local hash table (in storage_gtt.c)
that maps the GTT OID to a per-session RelFileLocator. Storage is
lazily created on first access via GttInitSessionStorage(), which is
called from RelationInitPhysicalAddr() when a GTT is encountered.
Per-session storage files use the temp-file naming convention
(t<ProcNumber>_<relfilenode>), so different sessions' files don't
collide. Files are automatically cleaned up at session exit via a
before_shmem_exit callback.
Hash entries are kept in sync with transaction state by xact and
subxact callbacks registered on first use:
- On subxact or xact abort, entries whose create_subid matches the
aborting subxact are removed; entries whose storage_subid matches
have storage_created cleared (the corresponding files are unlinked
by the normal PendingRelDelete machinery).
- On subxact commit, the tagged subids are reparented.
- On top-level commit, any entry marked drop_pending is removed.
GttScheduleDropSessionStorage() is called from heap_drop_with_catalog
to set the flag when a DROP TABLE runs on a GTT; this defers the
hash-entry teardown until the DROP actually commits, so an aborted
DROP leaves the entry intact.
GTTs are marked rd_islocaltemp=true in the relcache, so callers keyed
off that flag (the read-only-xact gate in COPY, extension-lock skips
in hio.c and the AM vacuum paths) treat them consistently with
regular temp tables. Protecting a GTT against concurrent DROP/ALTER
while a peer session is idle with active data is deferred to a later
commit ("DDL safety via session-level locks").
Key changes:
- New storage_gtt.c/h: per-session storage hash, lifecycle, and
xact/subxact callbacks
- heap.c: skip shared storage creation for GTTs at CREATE time, and
call GttScheduleDropSessionStorage() from heap_drop_with_catalog
- relcache.c: redirect GTT physical address to session-local storage
and set rd_islocaltemp=true for GTT relcache entries (in both
RelationBuildDesc and RelationBuildLocalRelation, each with its own
case so permanent/unlogged relations keep rd_islocaltemp=false)
- bufmgr.c: route GTT buffer operations through local buffer path
using the new RELPERSISTENCE_IS_LOCAL() macro
- storage.c: use ProcNumberForTempRelations() for GTT storage
- dbsize.c: pg_relation_filepath on a GTT returns the current
session's per-session path if initialized, else NULL
- dbcommands.c: CREATE DATABASE's WAL_LOG strategy skips GTTs when
scanning the template's pg_class -- like local temp tables, they
have no file at their catalog locator to copy
- pg_class.h: add RELPERSISTENCE_IS_LOCAL() convenience macro
---
src/backend/access/table/tableam.c | 6 +
src/backend/access/transam/xloginsert.c | 6 +-
src/backend/catalog/Makefile | 1 +
src/backend/catalog/heap.c | 18 +
src/backend/catalog/meson.build | 1 +
src/backend/catalog/storage.c | 5 +-
src/backend/catalog/storage_gtt.c | 529 ++++++++++++++++++++++++
src/backend/commands/createas.c | 63 ++-
src/backend/commands/dbcommands.c | 7 +-
src/backend/commands/sequence.c | 13 +
src/backend/commands/tablecmds.c | 27 +-
src/backend/storage/buffer/bufmgr.c | 31 +-
src/backend/utils/adt/dbsize.c | 18 +-
src/backend/utils/cache/relcache.c | 37 +-
src/include/catalog/pg_class.h | 8 +
src/include/catalog/storage_gtt.h | 23 ++
src/include/utils/rel.h | 7 +-
src/tools/pgindent/typedefs.list | 1 +
18 files changed, 757 insertions(+), 44 deletions(-)
create mode 100644 src/backend/catalog/storage_gtt.c
create mode 100644 src/include/catalog/storage_gtt.h
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 68ff0966f1c..0a2cb72f331 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -30,6 +30,7 @@
#include "storage/bufmgr.h"
#include "storage/shmem.h"
#include "storage/smgr.h"
+#include "catalog/storage_gtt.h"
/*
* Constants to control the behavior of block allocation to parallel workers
@@ -682,6 +683,11 @@ table_block_relation_size(Relation rel, ForkNumber forkNumber)
{
uint64 nblocks = 0;
+ /* See RelationGetNumberOfBlocksInFork: unmaterialized GTTs are empty. */
+ if (RelationIsGlobalTemp(rel) &&
+ !GttHasSessionStorage(RelationGetRelid(rel)))
+ return 0;
+
/* InvalidForkNumber indicates returning the size for all forks */
if (forkNumber == InvalidForkNumber)
{
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b82b7d..3088f308e5d 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -561,11 +561,11 @@ XLogSimpleInsertInt64(RmgrId rmid, uint8 info, int64 value)
XLogRecPtr
XLogGetFakeLSN(Relation rel)
{
- if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(rel->rd_rel->relpersistence))
{
/*
- * Temporary relations are only accessible in our session, so a simple
- * backend-local counter will do.
+ * Temporary and global temporary relations are only accessible within
+ * our session, so a simple backend-local counter will do.
*/
static XLogRecPtr counter = FirstNormalUnloggedLSN;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 26fa0c9b18c..1d6e49169b1 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -47,6 +47,7 @@ OBJS = \
pg_tablespace.o \
pg_type.o \
storage.o \
+ storage_gtt.o \
toasting.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 88087654de9..0811dfed681 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -54,6 +54,7 @@
#include "catalog/pg_tablespace.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
+#include "catalog/storage_gtt.h"
#include "commands/tablecmds.h"
#include "commands/typecmds.h"
#include "common/int.h"
@@ -344,6 +345,14 @@ heap_create(const char *relname,
*/
if (!RelFileNumberIsValid(relfilenumber))
relfilenumber = relid;
+
+ /*
+ * Global temporary tables need a relfilenode in the catalog (used as
+ * the basis for per-session file naming), but don't create shared
+ * storage -- per-session storage is created lazily.
+ */
+ if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ create_storage = false;
}
/*
@@ -1901,6 +1910,15 @@ heap_drop_with_catalog(Oid relid)
if (RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
RelationDropStorage(rel);
+ /*
+ * For global temporary tables, also schedule release of the per-session
+ * hash entry and the session-level lock. Physical file unlinking goes
+ * through the normal PendingRelDelete path above (rd_locator has been
+ * redirected to the per-session locator).
+ */
+ if (RelationIsGlobalTemp(rel))
+ GttScheduleDropSessionStorage(relid);
+
/* ensure that stats are dropped if transaction commits */
pgstat_drop_relation(rel);
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 11d21c5ad6b..76059030fda 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -34,6 +34,7 @@ backend_sources += files(
'pg_tablespace.c',
'pg_type.c',
'storage.c',
+ 'storage_gtt.c',
'toasting.c',
)
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index febd8c246ca..ef21f89860b 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -135,8 +135,11 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
needs_wal = false;
break;
case RELPERSISTENCE_UNLOGGED:
+ procNumber = INVALID_PROC_NUMBER;
+ needs_wal = false;
+ break;
case RELPERSISTENCE_GLOBAL_TEMP:
- procNumber = INVALID_PROC_NUMBER;
+ procNumber = ProcNumberForTempRelations();
needs_wal = false;
break;
case RELPERSISTENCE_PERMANENT:
diff --git a/src/backend/catalog/storage_gtt.c b/src/backend/catalog/storage_gtt.c
new file mode 100644
index 00000000000..427e3e111e0
--- /dev/null
+++ b/src/backend/catalog/storage_gtt.c
@@ -0,0 +1,529 @@
+/*-------------------------------------------------------------------------
+ *
+ * storage_gtt.c
+ * Per-session storage management for global temporary tables.
+ *
+ * Global temporary tables have a shared catalog definition but per-session
+ * private data. Each backend that accesses a GTT gets its own local
+ * storage files, stored in local buffers like regular temp tables.
+ *
+ * The mapping from GTT OID to per-session RelFileLocator is maintained in
+ * a backend-local hash table (gtt_storage_hash). Storage is lazily
+ * created on first access and cleaned up at session end.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/storage_gtt.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/tableam.h"
+#include "access/xact.h"
+#include "catalog/pg_tablespace_d.h"
+#include "catalog/storage.h"
+#include "catalog/storage_gtt.h"
+#include "common/hashfn.h"
+#include "miscadmin.h"
+#include "nodes/pg_list.h"
+#include "storage/ipc.h"
+#include "storage/bufmgr.h"
+#include "storage/procnumber.h"
+#include "storage/smgr.h"
+#include "utils/hsearch.h"
+#include "utils/inval.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
+
+/*
+ * Per-session state for a single global temporary table.
+ *
+ * The SubTransactionId fields track which (sub)transaction most recently
+ * performed an action that must be undone on abort:
+ * - create_subid: subxact that added this entry to the hash
+ * (InvalidSubTransactionId once the entry has survived to top-level
+ * commit)
+ * - storage_subid: subxact that most recently called RelationCreateStorage
+ * On subxact or xact abort of a given subid, the corresponding state is
+ * reverted. On subxact commit, the subid is reparented. See
+ * gtt_subxact_callback / gtt_xact_callback.
+ */
+typedef struct GttStorageEntry
+{
+ Oid relid; /* GTT's pg_class OID (hash key) */
+ RelFileLocator locator; /* per-session physical storage location */
+ bool storage_created; /* has smgr file been created? */
+ bool drop_pending; /* entry scheduled for drop at xact commit */
+ SubTransactionId create_subid; /* subxact that added this entry */
+ SubTransactionId storage_subid; /* subxact that created current storage */
+} GttStorageEntry;
+
+/* Backend-local hash table: GTT OID -> GttStorageEntry */
+static HTAB *gtt_storage_hash = NULL;
+
+/*
+ * True when any entry carries rollback-sensitive state (a valid
+ * create_subid/storage_subid, or drop_pending), letting the xact/subxact
+ * callbacks skip their full-hash scans in the common case of a transaction
+ * that established no such state. Conservative: it is only cleared once a
+ * top-level transaction end has settled every entry.
+ */
+static bool gtt_xact_state_dirty = false;
+
+/* Local function prototypes */
+static void gtt_session_cleanup(int code, Datum arg);
+static void ensure_gtt_hash(void);
+static void gtt_xact_callback(XactEvent event, void *arg);
+static void gtt_subxact_callback(SubXactEvent event,
+ SubTransactionId mySubid,
+ SubTransactionId parentSubid,
+ void *arg);
+static void gtt_remove_entry(GttStorageEntry *entry);
+static void gtt_revert_storage(GttStorageEntry *entry);
+static void gtt_remove_relids(List *to_remove);
+static void gtt_init_entry(GttStorageEntry *entry, Relation relation);
+
+/*
+ * ensure_gtt_hash
+ * Create the backend-local hash table on first use.
+ */
+static void
+ensure_gtt_hash(void)
+{
+ HASHCTL hashctl;
+
+ if (gtt_storage_hash != NULL)
+ return;
+
+ hashctl.keysize = sizeof(Oid);
+ hashctl.entrysize = sizeof(GttStorageEntry);
+ hashctl.hcxt = TopMemoryContext;
+ gtt_storage_hash = hash_create("GTT storage hash",
+ 32, /* initial size */
+ &hashctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Register session cleanup to drop all per-session GTT storage files when
+ * the backend exits, and xact/subxact callbacks that keep the hash in
+ * sync with transaction state (see gtt_xact_callback). The hash is
+ * created exactly once per backend and never destroyed, so this cannot
+ * register twice.
+ */
+ before_shmem_exit(gtt_session_cleanup, (Datum) 0);
+ RegisterXactCallback(gtt_xact_callback, NULL);
+ RegisterSubXactCallback(gtt_subxact_callback, NULL);
+}
+
+/*
+ * GttInitSessionStorage
+ * Ensure per-session local storage exists for the given GTT relation.
+ *
+ * This is called from RelationInitPhysicalAddr when a GTT is being set up
+ * in the relcache. It creates a per-session storage file if one doesn't
+ * exist yet, and fills in the relation's rd_locator and rd_backend to
+ * point to the session-local file.
+ *
+ * The per-session relfilenode is allocated from the local OID counter
+ * (same mechanism as regular temp tables).
+ */
+void
+GttInitSessionStorage(Relation relation)
+{
+ GttStorageEntry *entry;
+ bool found;
+ Oid relid = RelationGetRelid(relation);
+
+ ensure_gtt_hash();
+
+ entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
+ &relid,
+ HASH_ENTER,
+ &found);
+
+ if (!found)
+ gtt_init_entry(entry, relation);
+
+ /* Point the relation at our per-session storage */
+ relation->rd_locator = entry->locator;
+ relation->rd_backend = ProcNumberForTempRelations();
+
+ /*
+ * No physical file is created here. Reads of unmaterialized storage
+ * complete without one (the zero-blocks short-circuits in
+ * bufmgr.c/tableam.c report the relation empty), so the file is deferred
+ * to GttEnsureSessionStorage at the first genuine data access.
+ */
+}
+
+/*
+ * gtt_init_entry
+ * Initialize a newly created per-session map entry from the relation's
+ * current catalog state.
+ */
+static void
+gtt_init_entry(GttStorageEntry *entry, Relation relation)
+{
+ /*
+ * Set up the locator using the catalog's tablespace and database, but in
+ * this backend's temp namespace.
+ */
+ if (relation->rd_rel->reltablespace)
+ entry->locator.spcOid = relation->rd_rel->reltablespace;
+ else
+ entry->locator.spcOid = MyDatabaseTableSpace;
+
+ if (entry->locator.spcOid == GLOBALTABLESPACE_OID)
+ entry->locator.dbOid = InvalidOid;
+ else
+ entry->locator.dbOid = MyDatabaseId;
+
+ /*
+ * Use the catalog relfilenode as the per-session relfilenode. Since each
+ * backend uses its own proc number as the backend ID in the file path
+ * (t_<procnum>_<relfilenode>), the same relfilenode won't collide between
+ * sessions.
+ */
+ entry->locator.relNumber = relation->rd_rel->relfilenode;
+ entry->storage_created = false;
+ entry->drop_pending = false;
+ entry->create_subid = GetCurrentSubTransactionId();
+ gtt_xact_state_dirty = true;
+ entry->storage_subid = InvalidSubTransactionId;
+}
+
+/*
+ * GttEnsureSessionStorage
+ * Materialize this session's physical storage for a GTT.
+ *
+ * Called the first time a GTT is genuinely accessed for data: it creates the
+ * per-session file (registered for delete-at-abort via RelationCreateStorage).
+ * Reads never call this -- an unmaterialized GTT reads as empty.
+ */
+void
+GttEnsureSessionStorage(Relation relation)
+{
+ GttStorageEntry *entry;
+ Oid relid = RelationGetRelid(relation);
+
+ Assert(RelationIsGlobalTemp(relation));
+
+ if (gtt_storage_hash == NULL)
+ elog(ERROR, "no per-session storage map for global temporary table \"%s\"",
+ RelationGetRelationName(relation));
+
+ entry = (GttStorageEntry *) hash_search(gtt_storage_hash, &relid,
+ HASH_FIND, NULL);
+ if (entry == NULL)
+ elog(ERROR, "no per-session storage entry for global temporary table \"%s\"",
+ RelationGetRelationName(relation));
+
+ if (entry->storage_created)
+ return;
+
+ RelationCreateStorage(entry->locator, RELPERSISTENCE_GLOBAL_TEMP, true);
+ entry->storage_created = true;
+ entry->storage_subid = GetCurrentSubTransactionId();
+ gtt_xact_state_dirty = true;
+}
+
+/*
+ * GttHasSessionStorage
+ * Check if the current session has initialized storage for a GTT.
+ *
+ * Used by pg_relation_filepath to decide whether to surface the current
+ * session's private file path for a GTT (or NULL when this session has
+ * not yet accessed it).
+ */
+bool
+GttHasSessionStorage(Oid relid)
+{
+ if (gtt_storage_hash == NULL)
+ return false;
+
+ return hash_search(gtt_storage_hash, &relid, HASH_FIND, NULL) != NULL;
+}
+
+/*
+ * gtt_remove_entry
+ * Release per-session state for a GTT and remove its hash entry.
+ *
+ * This is the common teardown path invoked from gtt_session_cleanup,
+ * from the commit-side of a scheduled drop, and from abort handling when
+ * an entry was created in the aborting (sub)transaction. Physical file
+ * removal via PendingRelDelete is handled separately (by smgr's abort
+ * cleanup or RelationDropStorage).
+ */
+static void
+gtt_remove_entry(GttStorageEntry *entry)
+{
+ Oid relid = entry->relid;
+
+ hash_search(gtt_storage_hash, &relid, HASH_REMOVE, NULL);
+}
+
+/*
+ * GttScheduleDropSessionStorage
+ * Mark a GTT's per-session entry for cleanup at xact commit.
+ *
+ * Called from heap_drop_with_catalog when a DROP TABLE runs on a GTT.
+ * The actual removal of the hash entry happens when the transaction
+ * commits (see gtt_xact_callback). If the transaction aborts, the drop
+ * is abandoned and the entry stays.
+ *
+ * Physical file removal is handled by RelationDropStorage (via the
+ * standard PendingRelDelete mechanism). This function only coordinates
+ * the session-local metadata.
+ */
+void
+GttScheduleDropSessionStorage(Oid relid)
+{
+ GttStorageEntry *entry;
+
+ if (gtt_storage_hash == NULL)
+ return;
+
+ entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
+ &relid,
+ HASH_FIND,
+ NULL);
+ if (entry != NULL)
+ {
+ entry->drop_pending = true;
+ gtt_xact_state_dirty = true;
+ }
+}
+
+/*
+ * gtt_revert_storage
+ * Undo lazily-created storage state on (sub)transaction abort.
+ *
+ * The files themselves have been unlinked by PendingRelDelete; reset the
+ * bookkeeping so the next access re-creates the storage.
+ */
+static void
+gtt_revert_storage(GttStorageEntry *entry)
+{
+ entry->storage_created = false;
+ entry->storage_subid = InvalidSubTransactionId;
+}
+
+/*
+ * gtt_remove_relids
+ * Remove the hash entries named by a list of relation OIDs.
+ *
+ * Shared tail of the xact and subxact callbacks: victims are collected
+ * during the hash scan (hash_seq_search is fragile if the current entry is
+ * deleted) and removed here afterwards.
+ */
+static void
+gtt_remove_relids(List *to_remove)
+{
+ GttStorageEntry *entry;
+
+ foreach_oid(relid, to_remove)
+ {
+ entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
+ &relid,
+ HASH_FIND,
+ NULL);
+ if (entry != NULL)
+ gtt_remove_entry(entry);
+ }
+ list_free(to_remove);
+}
+
+/*
+ * gtt_xact_callback
+ * Reconcile gtt_storage_hash with transaction completion.
+ *
+ * On top-level commit: finalise any scheduled drops (remove entries) and
+ * clear per-entry subxact state, since the surviving entries are now
+ * permanent for the session.
+ *
+ * On top-level abort: roll back any state that was established by the
+ * aborting transaction. Entries created in this xact are removed;
+ * entries whose storage was established in this xact have storage_created
+ * cleared — PendingRelDelete has already unlinked the associated files.
+ */
+static void
+gtt_xact_callback(XactEvent event, void *arg)
+{
+ HASH_SEQ_STATUS status;
+ GttStorageEntry *entry;
+ List *to_remove = NIL;
+ List *to_invalidate = NIL;
+ ListCell *lc;
+
+ if (gtt_storage_hash == NULL)
+ return;
+
+ if (event != XACT_EVENT_COMMIT && event != XACT_EVENT_ABORT &&
+ event != XACT_EVENT_PARALLEL_COMMIT &&
+ event != XACT_EVENT_PARALLEL_ABORT)
+ return;
+
+ /*
+ * The common case is a transaction that touched no GTT state at all;
+ * don't pay for a full-hash scan at every commit for the rest of the
+ * session's life just because a GTT was once used.
+ */
+ if (!gtt_xact_state_dirty)
+ return;
+
+ hash_seq_init(&status, gtt_storage_hash);
+ while ((entry = (GttStorageEntry *) hash_seq_search(&status)) != NULL)
+ {
+ bool remove = false;
+
+ if (event == XACT_EVENT_COMMIT ||
+ event == XACT_EVENT_PARALLEL_COMMIT)
+ {
+ if (entry->drop_pending)
+ remove = true;
+ else
+ {
+ entry->create_subid = InvalidSubTransactionId;
+ entry->storage_subid = InvalidSubTransactionId;
+ }
+ }
+ else
+ {
+ /*
+ * Top-level abort. Either remove the entry (if it was created in
+ * this xact) or clear storage_created (if storage was created in
+ * this xact). In both cases the per-session file has been
+ * unlinked by PendingRelDelete and the relcache still has a
+ * cached rd_locator pointing at it; force a relcache invalidation
+ * so the next access re-runs RelationInitPhysicalAddr ->
+ * GttInitSessionStorage and recreates the storage.
+ */
+ if (entry->create_subid != InvalidSubTransactionId)
+ {
+ remove = true;
+ to_invalidate = lappend_oid(to_invalidate, entry->relid);
+ }
+ else
+ {
+ if (entry->storage_subid != InvalidSubTransactionId)
+ {
+ gtt_revert_storage(entry);
+ to_invalidate = lappend_oid(to_invalidate, entry->relid);
+ }
+ entry->drop_pending = false;
+ }
+ }
+
+ if (remove)
+ to_remove = lappend_oid(to_remove, entry->relid);
+ }
+
+ gtt_remove_relids(to_remove);
+
+ foreach(lc, to_invalidate)
+ RelationCacheInvalidateEntry(lfirst_oid(lc));
+ list_free(to_invalidate);
+
+ /* every entry has now been settled */
+ gtt_xact_state_dirty = false;
+}
+
+/*
+ * gtt_subxact_callback
+ * Reconcile gtt_storage_hash with subtransaction completion.
+ *
+ * On subxact commit, reparent subxact-tagged state to the parent. On
+ * subxact abort, revert state established in the aborting subxact: whole
+ * entry for newly-created ones, storage_created for older entries that
+ * had new storage created in the aborting subxact.
+ */
+static void
+gtt_subxact_callback(SubXactEvent event,
+ SubTransactionId mySubid,
+ SubTransactionId parentSubid,
+ void *arg)
+{
+ HASH_SEQ_STATUS status;
+ GttStorageEntry *entry;
+ List *to_remove = NIL;
+ List *to_invalidate = NIL;
+ ListCell *lc;
+
+ if (gtt_storage_hash == NULL)
+ return;
+
+ if (event != SUBXACT_EVENT_COMMIT_SUB && event != SUBXACT_EVENT_ABORT_SUB)
+ return;
+
+ /* As in gtt_xact_callback, skip the scans if nothing can need work. */
+ if (!gtt_xact_state_dirty)
+ return;
+
+ hash_seq_init(&status, gtt_storage_hash);
+ while ((entry = (GttStorageEntry *) hash_seq_search(&status)) != NULL)
+ {
+ if (event == SUBXACT_EVENT_COMMIT_SUB)
+ {
+ if (entry->create_subid == mySubid)
+ entry->create_subid = parentSubid;
+ if (entry->storage_subid == mySubid)
+ entry->storage_subid = parentSubid;
+ }
+ else /* SUBXACT_EVENT_ABORT_SUB */
+ {
+ if (entry->create_subid == mySubid)
+ {
+ to_remove = lappend_oid(to_remove, entry->relid);
+ to_invalidate = lappend_oid(to_invalidate, entry->relid);
+ continue;
+ }
+ if (entry->storage_subid == mySubid)
+ {
+ gtt_revert_storage(entry);
+ to_invalidate = lappend_oid(to_invalidate, entry->relid);
+ }
+ }
+ }
+
+ gtt_remove_relids(to_remove);
+
+ /* See gtt_xact_callback: invalidate relcache for any killed storage. */
+ foreach(lc, to_invalidate)
+ RelationCacheInvalidateEntry(lfirst_oid(lc));
+ list_free(to_invalidate);
+}
+
+/*
+ * gtt_session_cleanup
+ * Drop all per-session GTT storage files at backend exit.
+ *
+ * This runs from before_shmem_exit. Entries scheduled for drop by a
+ * committed DROP TABLE have already been removed by gtt_xact_callback,
+ * so anything still in the hash represents live per-session storage
+ * that should be unlinked.
+ */
+static void
+gtt_session_cleanup(int code, Datum arg)
+{
+ HASH_SEQ_STATUS status;
+ GttStorageEntry *entry;
+
+ if (gtt_storage_hash == NULL)
+ return;
+
+ hash_seq_init(&status, gtt_storage_hash);
+ while ((entry = (GttStorageEntry *) hash_seq_search(&status)) != NULL)
+ {
+ if (entry->storage_created)
+ {
+ SMgrRelation srel;
+
+ srel = smgropen(entry->locator, ProcNumberForTempRelations());
+ smgrdounlinkall(&srel, 1, false);
+ }
+ }
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..558364c2af6 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -28,7 +28,10 @@
#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/xact.h"
+#include "access/genam.h"
#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
+#include "catalog/storage_gtt.h"
#include "catalog/toasting.h"
#include "commands/createas.h"
#include "commands/matview.h"
@@ -302,6 +305,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
List *rewritten;
PlannedStmt *plan;
QueryDesc *queryDesc;
+ int cursorOptions;
Assert(!is_matview);
@@ -319,9 +323,18 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
query = linitial_node(Query, rewritten);
Assert(query->commandType == CMD_SELECT);
- /* plan the query */
+ /*
+ * Plan the query. The inserting query may normally run in parallel,
+ * but a global temporary table's per-session storage is created
+ * lazily on the first write, which cannot happen in parallel mode.
+ * The target relation is not in the query's range table, so
+ * max_parallel_hazard() can't see it; suppress parallelism here.
+ */
+ cursorOptions = CURSOR_OPT_PARALLEL_OK;
+ if (into->rel && into->rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ cursorOptions = 0;
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, params, NULL);
+ cursorOptions, params, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
@@ -532,6 +545,52 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
*/
intoRelationDesc = table_open(intoRelationAddr.objectId, AccessExclusiveLock);
+ /*
+ * For global temporary tables, pre-materialize the per-session storage
+ * now, before the executor may enter parallel mode. The SELECT variant
+ * of CTAS already suppresses parallel planning via cursorOptions=0, but
+ * the EXECUTE variant uses a pre-compiled plan that may have
+ * parallelModeNeeded=true (e.g. under debug_parallel_query=regress).
+ * GttEnsureSessionStorage would be called lazily on the first write, but
+ * RelationCreateStorage asserts !IsInParallelMode(), so we force
+ * materialization here while we are still outside parallel mode.
+ *
+ * We also pre-materialize the TOAST table (which inherits GTT persistence)
+ * and trigger its deferred index build. Without this, a CTAS producing
+ * wide rows could fail when heap_insert opens the TOAST table and calls
+ * GttEnsureSessionStorage in the middle of parallel execution.
+ */
+ if (!into->skipData && RelationIsGlobalTemp(intoRelationDesc))
+ {
+ GttEnsureSessionStorage(intoRelationDesc);
+ if (OidIsValid(intoRelationDesc->rd_rel->reltoastrelid))
+ {
+ Relation toastrel;
+ List *indexlist;
+ ListCell *ilc;
+
+ toastrel = table_open(intoRelationDesc->rd_rel->reltoastrelid,
+ AccessShareLock);
+ GttEnsureSessionStorage(toastrel);
+
+ /*
+ * Opening each TOAST index triggers GttBuildIndexIfNeeded, which
+ * will find the TOAST heap storage materialized and build_deferred
+ * set (set during create_ctas_internal because the heap was then
+ * empty), and so will materialize and initialize the index.
+ */
+ indexlist = RelationGetIndexList(toastrel);
+ foreach(ilc, indexlist)
+ {
+ Relation idxrel = index_open(lfirst_oid(ilc), AccessShareLock);
+
+ index_close(idxrel, AccessShareLock);
+ }
+ list_free(indexlist);
+ table_close(toastrel, AccessShareLock);
+ }
+ }
+
/*
* Make sure the constructed table does not have RLS enabled.
*
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..17198b1cf72 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -410,11 +410,14 @@ ScanSourceDatabasePgClassTuple(HeapTupleData *tuple, Oid tbid, Oid dbid,
* are inaccessible outside of the session that created them, which must
* be gone already, and couldn't connect to a different database if it
* still existed. autovacuum will eventually remove the pg_class entries
- * as well.
+ * as well. Global temporary tables have no file at their catalog locator
+ * (per-session storage is created lazily in each backend's temp
+ * namespace), so there is nothing to copy for them either; their catalog
+ * definitions travel with pg_class itself.
*/
if (classForm->reltablespace == GLOBALTABLESPACE_OID ||
!RELKIND_HAS_STORAGE(classForm->relkind) ||
- classForm->relpersistence == RELPERSISTENCE_TEMP)
+ RELPERSISTENCE_IS_LOCAL(classForm->relpersistence))
return NULL;
/*
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 8eef726a228..15ad0490ac1 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -48,6 +48,7 @@
#include "utils/resowner.h"
#include "utils/syscache.h"
#include "utils/varlena.h"
+#include "catalog/storage_gtt.h"
/*
@@ -330,6 +331,10 @@ ResetSequence(Oid seq_relid)
static void
fill_seq_with_data(Relation rel, HeapTuple tuple)
{
+ /* a GTT sequence's per-session storage is created lazily */
+ if (rel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ GttEnsureSessionStorage(rel);
+
fill_seq_fork_with_data(rel, tuple, MAIN_FORKNUM);
if (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED)
@@ -446,6 +451,14 @@ GttEnsureSequenceInitialized(Relation rel)
if (rel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
return;
+ /*
+ * Sequences are the documented exception to lazy storage creation: the
+ * one-row contract (SELECT last_value FROM seq, psql's \d) requires a
+ * readable row, so opening a GTT sequence materializes its one-page file
+ * and seeds it.
+ */
+ GttEnsureSessionStorage(rel);
+
if (RelationGetNumberOfBlocks(rel) > 0)
return;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 82700c49ba0..d4e83e5658c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -843,8 +843,7 @@ 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_GLOBAL_TEMP)
+ && !RELPERSISTENCE_IS_LOCAL(stmt->relation->relpersistence))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("ON COMMIT can only be used on temporary tables")));
@@ -2785,20 +2784,16 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* that inheritance allows that case.
*/
if (is_partition &&
- relation->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
- relation->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
- (relpersistence == RELPERSISTENCE_TEMP ||
- relpersistence == RELPERSISTENCE_GLOBAL_TEMP))
+ !RELPERSISTENCE_IS_LOCAL(relation->rd_rel->relpersistence) &&
+ RELPERSISTENCE_IS_LOCAL(relpersistence))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
RelationGetRelationName(relation))));
/* Permanent rels cannot inherit from temporary ones */
- if (relpersistence != RELPERSISTENCE_TEMP &&
- relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
- (relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
- RelationIsGlobalTemp(relation)))
+ if (!RELPERSISTENCE_IS_LOCAL(relpersistence) &&
+ RELPERSISTENCE_IS_LOCAL(relation->rd_rel->relpersistence))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg(!is_partition
@@ -20783,20 +20778,16 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
RelationGetRelationName(attachrel))));
/* If the parent is permanent, so must be all of its partitions. */
- if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
- rel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP &&
- (attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
- RelationIsGlobalTemp(attachrel)))
+ if (!RELPERSISTENCE_IS_LOCAL(rel->rd_rel->relpersistence) &&
+ RELPERSISTENCE_IS_LOCAL(attachrel->rd_rel->relpersistence))
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 ||
- RelationIsGlobalTemp(rel)) &&
- attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
- attachrel->rd_rel->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(rel->rd_rel->relpersistence) &&
+ !RELPERSISTENCE_IS_LOCAL(attachrel->rd_rel->relpersistence))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cd0bcdf2fe8..66f0c6586f1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -44,6 +44,7 @@
#include "catalog/pg_tablespace_d.h"
#endif
#include "catalog/storage.h"
+#include "catalog/storage_gtt.h"
#include "catalog/storage_xlog.h"
#include "common/hashfn.h"
#include "executor/instrument.h"
@@ -1246,7 +1247,7 @@ PinBufferForBlock(Relation rel,
smgr->smgr_rlocator.locator.relNumber,
smgr->smgr_rlocator.backend);
- if (persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(persistence))
bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
else
bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
@@ -1328,7 +1329,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
IOContext io_context;
IOObject io_object;
- if (persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(persistence))
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1392,7 +1393,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot access temporary tables of other sessions")));
- if (operation->persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(operation->persistence))
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1693,7 +1694,7 @@ TrackBufferHit(IOObject io_object, IOContext io_context,
smgr->smgr_rlocator.backend,
true);
- if (persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(persistence))
pgBufferUsage.local_blks_hit += 1;
else
pgBufferUsage.shared_blks_hit += 1;
@@ -1764,7 +1765,7 @@ WaitReadBuffers(ReadBuffersOperation *operation)
IOObject io_object;
bool needed_wait = false;
- if (operation->persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(operation->persistence))
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1954,7 +1955,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
instr_time io_start;
StartBufferIOResult status;
- if (persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(persistence))
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1973,7 +1974,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
if (flags & READ_BUFFERS_SYNCHRONOUSLY)
ioh_flags |= PGAIO_HF_SYNCHRONOUS;
- if (persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(persistence))
ioh_flags |= PGAIO_HF_REFERENCES_LOCAL;
/*
@@ -2134,7 +2135,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
pgaio_io_set_handle_data_32(ioh, (uint32 *) io_buffers, io_buffers_len);
pgaio_io_register_callbacks(ioh,
- persistence == RELPERSISTENCE_TEMP ?
+ RELPERSISTENCE_IS_LOCAL(persistence) ?
PGAIO_HCB_LOCAL_BUFFER_READV :
PGAIO_HCB_SHARED_BUFFER_READV,
flags);
@@ -2157,7 +2158,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
pgstat_count_io_op_time(io_object, io_context, IOOP_READ,
io_start, 1, io_buffers_len * BLCKSZ);
- if (persistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(persistence))
pgBufferUsage.local_blks_read += io_buffers_len;
else
pgBufferUsage.shared_blks_read += io_buffers_len;
@@ -2767,7 +2768,7 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
BMR_GET_SMGR(bmr)->smgr_rlocator.backend,
extend_by);
- if (bmr.relpersistence == RELPERSISTENCE_TEMP)
+ if (RELPERSISTENCE_IS_LOCAL(bmr.relpersistence))
first_block = ExtendBufferedRelLocal(bmr, fork, flags,
extend_by, extend_upto,
buffers, &extend_by);
@@ -4654,6 +4655,16 @@ FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln,
BlockNumber
RelationGetNumberOfBlocksInFork(Relation relation, ForkNumber forkNum)
{
+ /*
+ * A global temporary table whose per-session storage has not been
+ * materialized has no file: it is empty by definition. Reporting zero
+ * blocks here (and in table_block_relation_size) is what lets reads of
+ * never-written GTTs complete without creating any storage.
+ */
+ if (RelationIsGlobalTemp(relation) &&
+ !GttHasSessionStorage(RelationGetRelid(relation)))
+ return 0;
+
if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
{
/*
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 7243ec0dfd9..93b45133adf 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/storage_gtt.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
#include "storage/fd.h"
@@ -1020,9 +1021,24 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
{
case RELPERSISTENCE_UNLOGGED:
case RELPERSISTENCE_PERMANENT:
- case RELPERSISTENCE_GLOBAL_TEMP:
backend = INVALID_PROC_NUMBER;
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+
+ /*
+ * GTTs have no shared storage; each backend has a private file
+ * named after its own proc number. If the current session has
+ * initialized this GTT, report that file; otherwise NULL,
+ * matching the "not yet accessed" state.
+ */
+ if (GttHasSessionStorage(relid))
+ backend = ProcNumberForTempRelations();
+ else
+ {
+ ReleaseSysCache(tuple);
+ PG_RETURN_NULL();
+ }
+ break;
case RELPERSISTENCE_TEMP:
if (isTempOrTempToastNamespace(relform->relnamespace))
backend = ProcNumberForTempRelations();
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d0219396cc4..3ffed51f9e9 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -64,6 +64,7 @@
#include "catalog/pg_type.h"
#include "catalog/schemapg.h"
#include "catalog/storage.h"
+#include "catalog/storage_gtt.h"
#include "commands/policy.h"
#include "commands/publicationcmds.h"
#include "commands/trigger.h"
@@ -1158,10 +1159,23 @@ retry:
{
case RELPERSISTENCE_UNLOGGED:
case RELPERSISTENCE_PERMANENT:
- case RELPERSISTENCE_GLOBAL_TEMP:
relation->rd_backend = INVALID_PROC_NUMBER;
relation->rd_islocaltemp = false;
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+
+ /*
+ * GTT data is per-session: no other backend can see our rows.
+ * Mark rd_islocaltemp so callers keyed off that flag (the
+ * read-only-xact gate in COPY, extension-lock skips in hio.c and
+ * the AM vacuum paths, etc.) treat GTTs consistently with regular
+ * temp tables. rd_backend is left INVALID_PROC_NUMBER here and
+ * will be set to ProcNumberForTempRelations by
+ * GttInitSessionStorage when physical-address init runs.
+ */
+ relation->rd_backend = INVALID_PROC_NUMBER;
+ relation->rd_islocaltemp = true;
+ break;
case RELPERSISTENCE_TEMP:
if (isTempOrTempToastNamespace(relation->rd_rel->relnamespace))
{
@@ -1341,6 +1355,16 @@ RelationInitPhysicalAddr(Relation relation)
if (!RELKIND_HAS_STORAGE(relation->rd_rel->relkind))
return;
+ /*
+ * Global temporary tables use per-session local storage. Redirect the
+ * relation's physical address to the session-local file.
+ */
+ if (RelationIsGlobalTemp(relation))
+ {
+ GttInitSessionStorage(relation);
+ return;
+ }
+
if (relation->rd_rel->reltablespace)
relation->rd_locator.spcOid = relation->rd_rel->reltablespace;
else
@@ -3654,10 +3678,19 @@ 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;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+
+ /*
+ * As in RelationBuildDesc: GTT data is per-session, so mark
+ * rd_islocaltemp; rd_backend is set by GttInitSessionStorage when
+ * physical-address init runs.
+ */
+ rel->rd_backend = INVALID_PROC_NUMBER;
+ rel->rd_islocaltemp = true;
+ break;
case RELPERSISTENCE_TEMP:
Assert(isTempOrTempToastNamespace(relnamespace));
rel->rd_backend = ProcNumberForTempRelations();
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 1f7ea2bf00b..7a811a26bdc 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -185,6 +185,14 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);
#define RELPERSISTENCE_TEMP 't' /* temporary table */
#define RELPERSISTENCE_GLOBAL_TEMP 'g' /* global temporary table */
+/*
+ * Does this persistence type use local (per-backend) buffers?
+ * Both session-local temp tables and global temporary tables store
+ * their data in local buffers.
+ */
+#define RELPERSISTENCE_IS_LOCAL(p) \
+ ((p) == RELPERSISTENCE_TEMP || (p) == RELPERSISTENCE_GLOBAL_TEMP)
+
/* default selection for replica identity (primary key or nothing) */
#define REPLICA_IDENTITY_DEFAULT 'd'
/* no replica identity is logged for this relation */
diff --git a/src/include/catalog/storage_gtt.h b/src/include/catalog/storage_gtt.h
new file mode 100644
index 00000000000..cefa5b5dbca
--- /dev/null
+++ b/src/include/catalog/storage_gtt.h
@@ -0,0 +1,23 @@
+/*-------------------------------------------------------------------------
+ *
+ * storage_gtt.h
+ * Per-session storage management for global temporary tables.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/storage_gtt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef STORAGE_GTT_H
+#define STORAGE_GTT_H
+
+#include "utils/rel.h"
+
+extern void GttInitSessionStorage(Relation relation);
+extern void GttEnsureSessionStorage(Relation relation);
+extern bool GttHasSessionStorage(Oid relid);
+extern void GttScheduleDropSessionStorage(Oid relid);
+
+#endif /* STORAGE_GTT_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 246cbfd49ba..696591b7d22 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -651,13 +651,10 @@ RelationCloseSmgr(Relation relation)
/*
* 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)
+ ((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP || \
+ (relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
/*
* RELATION_IS_LOCAL
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..6ef53535c7e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1187,6 +1187,7 @@ GroupingSet
GroupingSetData
GroupingSetKind
GroupingSetsPath
+GttStorageEntry
GucAction
GucBoolAssignHook
GucBoolCheckHook
--
2.43.0