From 7c04a7b997b6ae185b05cb7987dfb976e990cee5 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan 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_), 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__), 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