From 823eaee770c9a394a6d3f53e98040a67e73b9461 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan 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 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 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 [ (...) ] 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