0010-Global-temporary-tables-pg_dump-psql-and-replication.patch
text/x-patch
Filename: 0010-Global-temporary-tables-pg_dump-psql-and-replication.patch
Type: text/x-patch
Part: 9
Message:
Re: Global temporary tables
Patch
Format: format-patch
Series: patch 0010
Subject: Global temporary tables: pg_dump, psql, and replication support
| File | + | − |
|---|---|---|
| src/backend/catalog/pg_publication.c | 9 | 1 |
| src/bin/pg_dump/pg_dump.c | 56 | 11 |
| src/bin/pg_dump/pg_dump.h | 1 | 0 |
| src/bin/pg_upgrade/info.c | 2 | 0 |
| src/bin/psql/describe.c | 6 | 0 |
| src/bin/psql/tab-complete.in.c | 23 | 5 |
From 823eaee770c9a394a6d3f53e98040a67e73b9461 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Fri, 13 Mar 2026 13:23:00 -0400
Subject: [PATCH 10/12] Global temporary tables: pg_dump, psql, and replication
support
pg_dump:
- Never dump data for GTTs (data is per-session and ephemeral).
- Emit CREATE GLOBAL TEMPORARY TABLE with proper ON COMMIT clause
(DELETE ROWS or PRESERVE ROWS) instead of leaking the internal
on_commit_delete reloption into the WITH(...) clause.
- Extract on_commit_delete from reloptions in the catalog query,
following the same array_remove pattern used for check_option.
- Emit CREATE GLOBAL TEMPORARY SEQUENCE for sequences with
RELPERSISTENCE_GLOBAL_TEMP, mirroring the UNLOGGED prefix path.
psql:
- Show "Global temporary table" and "Global temporary index" labels
in \d output, matching the existing "Unlogged table" pattern.
- The \dt+ verbose listing already showed "global temporary" in the
Persistence column.
Replication:
- FOR ALL TABLES publications already exclude GTTs because
is_publishable_class() requires RELPERSISTENCE_PERMANENT.
- Explicit CREATE PUBLICATION FOR TABLE and ALTER PUBLICATION ADD
TABLE now reject GTTs in check_publication_add_relation, matching
the existing treatment of TEMP and UNLOGGED relations.
- Physical replication only sees the catalog definition since GTT
data uses local buffers and generates no WAL.
- pg_dump skips relation/attribute statistics for GTTs (their
statistics are per-session; the shared pg_class/pg_statistic rows
are never populated, and pg_restore_relation_stats() refuses to
write shared statistics for them on restore).
- pg_upgrade excludes GTTs from the file transfer map: they have no
files at their catalog locators, and their definitions travel with
the pg_dump schema restore. Per-session storage is recreated
lazily in the new cluster.
---
src/backend/catalog/pg_publication.c | 10 ++++-
src/bin/pg_dump/pg_dump.c | 67 +++++++++++++++++++++++-----
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_upgrade/info.c | 2 +
src/bin/psql/describe.c | 6 +++
src/bin/psql/tab-complete.in.c | 28 +++++++++---
6 files changed, 97 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 5c457d9aca8..9717267273a 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -92,7 +92,10 @@ check_publication_add_relation(PublicationRelInfo *pri)
errmsg(errormsg, relname),
errdetail("This operation is not supported for system tables.")));
- /* UNLOGGED and TEMP relations cannot be part of publication. */
+ /*
+ * UNLOGGED, TEMP, and GLOBAL TEMP relations cannot be part of
+ * publication.
+ */
if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -103,6 +106,11 @@ check_publication_add_relation(PublicationRelInfo *pri)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg(errormsg, relname),
errdetail("This operation is not supported for unlogged tables.")));
+ else if (RelationIsGlobalTemp(targetrel))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg(errormsg, RelationGetRelationName(targetrel)),
+ errdetail("This operation is not supported for global temporary tables."));
}
/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 0c42d81a0be..d97352e1306 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3050,6 +3050,10 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
if (tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
return;
+ /* Never dump data for global temporary tables (data is per-session) */
+ if (tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ return;
+
/* Don't dump data in unlogged tables, if so requested */
if (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED &&
dopt->no_unlogged_table_data)
@@ -7315,6 +7319,7 @@ getTables(Archive *fout, int *numTables)
int i_toastminmxid;
int i_reloptions;
int i_checkoption;
+ int i_gtt_on_commit_delete;
int i_toastreloptions;
int i_reloftype;
int i_foreignserver;
@@ -7409,12 +7414,24 @@ getTables(Archive *fout, int *numTables)
if (fout->remoteVersion >= 90300)
appendPQExpBufferStr(query,
- "array_remove(array_remove(c.reloptions,'check_option=local'),'check_option=cascaded') AS reloptions, "
+
+ /*
+ * Filter out reloptions that are internal to the server and shouldn't
+ * be emitted as user-visible WITH(...) items. Keyed by prefix so
+ * that a future change to how these options are serialised (different
+ * value spellings, added keys) doesn't silently break pg_dump.
+ */
+ "ARRAY(SELECT r FROM unnest(c.reloptions) AS r "
+ "WHERE r NOT LIKE 'check_option=%' "
+ " AND r NOT LIKE 'on_commit_delete=%') "
+ "AS reloptions, "
"CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text "
- "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, ");
+ "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, "
+ "('on_commit_delete=true' = ANY (c.reloptions)) AS gtt_on_commit_delete, ");
else
appendPQExpBufferStr(query,
- "c.reloptions, NULL AS checkoption, ");
+ "c.reloptions, NULL AS checkoption, "
+ "false AS gtt_on_commit_delete, ");
if (fout->remoteVersion >= 90600)
appendPQExpBufferStr(query,
@@ -7540,6 +7557,7 @@ getTables(Archive *fout, int *numTables)
i_toastminmxid = PQfnumber(res, "tminmxid");
i_reloptions = PQfnumber(res, "reloptions");
i_checkoption = PQfnumber(res, "checkoption");
+ i_gtt_on_commit_delete = PQfnumber(res, "gtt_on_commit_delete");
i_toastreloptions = PQfnumber(res, "toast_reloptions");
i_reloftype = PQfnumber(res, "reloftype");
i_foreignserver = PQfnumber(res, "foreignserver");
@@ -7621,6 +7639,8 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].checkoption = NULL;
else
tblinfo[i].checkoption = pg_strdup(PQgetvalue(res, i, i_checkoption));
+ tblinfo[i].gtt_on_commit_delete =
+ (strcmp(PQgetvalue(res, i, i_gtt_on_commit_delete), "t") == 0);
tblinfo[i].toast_reloptions = pg_strdup(PQgetvalue(res, i, i_toastreloptions));
tblinfo[i].reloftype = atooid(PQgetvalue(res, i, i_reloftype));
tblinfo[i].foreign_server = atooid(PQgetvalue(res, i, i_foreignserver));
@@ -7668,8 +7688,14 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
tblinfo[i].hascolumnACLs = false; /* may get set later */
- /* Add statistics */
- if (tblinfo[i].interesting)
+ /*
+ * Add statistics. Global temporary tables are skipped: their
+ * statistics are per-session (the shared pg_class/pg_statistic rows
+ * are never populated), and pg_restore_relation_stats() refuses to
+ * write shared statistics for them on restore.
+ */
+ if (tblinfo[i].interesting &&
+ tblinfo[i].relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
{
RelStatsInfo *stats;
@@ -8240,10 +8266,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
pg_fatal("could not parse %s array", "indattnames");
}
- relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
- PQgetvalue(res, j, i_reltuples),
- relallvisible, relallfrozen, indexkind,
- indAttNames, nindAttNames);
+ /* as in getTables, no statistics for global temp tables */
+ if (tbinfo->relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
+ relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
+ PQgetvalue(res, j, i_reltuples),
+ relallvisible, relallfrozen, indexkind,
+ indAttNames, nindAttNames);
+ else
+ relstats = NULL;
contype = *(PQgetvalue(res, j, i_contype));
if (contype == 'p' || contype == 'u' || contype == 'x')
@@ -17712,6 +17742,15 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
if (ftoptions && ftoptions[0])
appendPQExpBuffer(q, "\nOPTIONS (\n %s\n)", ftoptions);
+ /* Emit ON COMMIT clause for global temporary tables */
+ if (tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ {
+ if (tbinfo->gtt_on_commit_delete)
+ appendPQExpBufferStr(q, "\nON COMMIT DELETE ROWS");
+ else
+ appendPQExpBufferStr(q, "\nON COMMIT PRESERVE ROWS");
+ }
+
/*
* For materialized views, create the AS clause just like a view. At
* this point, we always mark the view as not populated.
@@ -19606,10 +19645,16 @@ dumpSequence(Archive *fout, const TableInfo *tbinfo)
}
else
{
+ const char *persistence_prefix = "";
+
+ if (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED)
+ persistence_prefix = "UNLOGGED ";
+ else if (tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ persistence_prefix = "GLOBAL TEMPORARY ";
+
appendPQExpBuffer(query,
"CREATE %sSEQUENCE %s\n",
- tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED ?
- "UNLOGGED " : "",
+ persistence_prefix,
fmtQualifiedDumpable(tbinfo));
if (seq->seqtype != SEQTYPE_BIGINT)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5a6726d8b12..258a461c05f 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -315,6 +315,7 @@ typedef struct _tableInfo
char *reloptions; /* options specified by WITH (...) */
char *checkoption; /* WITH CHECK OPTION, if any */
char *toast_reloptions; /* WITH options for the TOAST table */
+ bool gtt_on_commit_delete; /* GTT: ON COMMIT DELETE ROWS? */
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 37fff93892f..fa2bb736cee 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -503,6 +503,8 @@ get_rel_infos_query(void)
" ON c.relnamespace = n.oid "
" WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
CppAsString2(RELKIND_MATVIEW) "%s) AND "
+ /* global temp tables have no persistent storage to transfer */
+ " c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " AND "
/* exclude possible orphaned temp tables */
" ((n.nspname !~ '^pg_temp_' AND "
" n.nspname !~ '^pg_toast_temp_' AND "
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a33b6a0bcab..a5b3b021d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2138,6 +2138,9 @@ describeOneTableDetails(const char *schemaname,
if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
printfPQExpBuffer(&title, _("Unlogged table \"%s.%s\""),
schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ printfPQExpBuffer(&title, _("Global temporary table \"%s.%s\""),
+ schemaname, relationname);
else
printfPQExpBuffer(&title, _("Table \"%s.%s\""),
schemaname, relationname);
@@ -2154,6 +2157,9 @@ describeOneTableDetails(const char *schemaname,
if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
printfPQExpBuffer(&title, _("Unlogged index \"%s.%s\""),
schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ printfPQExpBuffer(&title, _("Global temporary index \"%s.%s\""),
+ schemaname, relationname);
else
printfPQExpBuffer(&title, _("Index \"%s.%s\""),
schemaname, relationname);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index de547a8cb37..4db222bfeb2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1332,6 +1332,7 @@ static const pgsql_thing_t words_after_create[] = {
{"EXTENSION", Query_for_list_of_extensions},
{"FOREIGN DATA WRAPPER", NULL, NULL, NULL},
{"FOREIGN TABLE", NULL, NULL, NULL},
+ {"GLOBAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE GLOBAL TEMP TABLE ... */
{"FUNCTION", NULL, NULL, Query_for_list_of_functions},
{"GROUP", Query_for_list_of_roles},
{"INDEX", NULL, NULL, &Query_for_list_of_indexes},
@@ -3861,6 +3862,12 @@ match_previous_words(int pattern_id,
/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
+ /* Complete "CREATE GLOBAL" with TEMP or TEMPORARY */
+ else if (TailMatches("CREATE", "GLOBAL"))
+ COMPLETE_WITH("TEMP", "TEMPORARY");
+ /* Complete "CREATE GLOBAL TEMP/TEMPORARY" with TABLE */
+ else if (TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY"))
+ COMPLETE_WITH("TABLE");
/* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */
else if (TailMatches("CREATE", "UNLOGGED"))
COMPLETE_WITH("TABLE", "SEQUENCE");
@@ -3875,17 +3882,21 @@ match_previous_words(int pattern_id,
COMPLETE_WITH("FOR VALUES", "DEFAULT");
/* Complete CREATE TABLE <name> with '(', AS, OF or PARTITION OF */
else if (TailMatches("CREATE", "TABLE", MatchAny) ||
- TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny))
+ TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny) ||
+ TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny))
COMPLETE_WITH("(", "AS", "OF", "PARTITION OF");
/* Complete CREATE TABLE <name> OF with list of composite types */
else if (TailMatches("CREATE", "TABLE", MatchAny, "OF") ||
- TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF"))
+ TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF") ||
+ TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "OF"))
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
/* Complete CREATE TABLE <name> [ (...) ] AS with list of keywords */
else if (TailMatches("CREATE", "TABLE", MatchAny, "AS") ||
TailMatches("CREATE", "TABLE", MatchAny, "(*)", "AS") ||
TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "AS") ||
- TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "AS"))
+ TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "AS") ||
+ TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "AS") ||
+ TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "AS"))
COMPLETE_WITH("EXECUTE", "SELECT", "TABLE", "VALUES", "WITH");
/* Complete CREATE TABLE name (...) with supported options */
else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)"))
@@ -3895,17 +3906,24 @@ match_previous_words(int pattern_id,
else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)"))
COMPLETE_WITH("AS", "INHERITS (", "ON COMMIT", "PARTITION BY", "USING",
"TABLESPACE", "WITH (");
+ else if (TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)"))
+ COMPLETE_WITH("AS", "INHERITS (", "ON COMMIT", "PARTITION BY", "USING",
+ "TABLESPACE", "WITH (");
/* Complete CREATE TABLE (...) USING with table access methods */
else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)", "USING") ||
- TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "USING"))
+ TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "USING") ||
+ TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "USING"))
COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
/* Complete CREATE TABLE (...) WITH with storage parameters */
else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)", "WITH", "(") ||
- TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "WITH", "("))
+ TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "WITH", "(") ||
+ TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "WITH", "("))
COMPLETE_WITH_LIST(table_storage_parameters);
/* Complete CREATE TABLE ON COMMIT with actions */
else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "ON", "COMMIT"))
COMPLETE_WITH("DELETE ROWS", "DROP", "PRESERVE ROWS");
+ else if (TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "ON", "COMMIT"))
+ COMPLETE_WITH("DELETE ROWS", "DROP", "PRESERVE ROWS");
/* CREATE TABLESPACE */
else if (Matches("CREATE", "TABLESPACE", MatchAny))
--
2.43.0