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